Skip to content

fix: use skipAutoInitialize to prevent SSR token refresh race condition#131

Merged
mandarini merged 12 commits intomainfrom
fix/async-token-refresh-race-condition
Feb 19, 2026
Merged

fix: use skipAutoInitialize to prevent SSR token refresh race condition#131
mandarini merged 12 commits intomainfrom
fix/async-token-refresh-race-condition

Conversation

@mandarini
Copy link
Contributor

@mandarini mandarini commented Oct 14, 2025

Problem

In SSR contexts, the Supabase auth client constructor automatically initializes and may trigger token refresh asynchronously. If this completes after the HTTP response has been generated, cookie setting fails with:

      Error: Cannot use `cookies.set(...)` after the response has been generated              

Solution

Set skipAutoInitialize: true when creating the server client. This prevents GoTrue's constructor from auto-initializing, which is the correct behavior for SSR — GoTrue's own docs note that all public methods have lazy initialization so the client remains fully functional without it.

No user-facing initialize() call is needed. getSession() handles session loading and token refresh lazily, as it always has.

@mandarini mandarini force-pushed the fix/async-token-refresh-race-condition branch from dc11250 to 9c556eb Compare October 14, 2025 10:48
@mandarini mandarini requested a review from hf October 14, 2025 14:22
@mandarini mandarini self-assigned this Oct 14, 2025
@ixxie
Copy link

ixxie commented Dec 14, 2025

@mandarini thanks for opening this, I'm running into this issue since I had to upgrade from Vite 5 => 7 in a SvelteKit project.

Important

Svelte and SvelteKit are moving really fast right now, since they have experimental support for async Svelte and SvelteKit remote functions. So SvelteKit users that want to keep dependencies up to day have to bump Vite to 7.

Is there a workaround I could do to the recommended approach from the docs to sidestep the issue? The best I could come up with (with Claude's help) is:

export const handle: Handle = async ({ event, resolve }) => {
  // Creates a Supabase client specific to this server request.
  // The Supabase client gets the Auth token from the request cookies.
  event.locals.supabase = createServerClient(
    PUBLIC_SUPABASE_URL,
    PUBLIC_SUPABASE_ANON_KEY,
    {
      cookies: {
        getAll: () => event.cookies.getAll(),
        // SvelteKit requires cookie `path` to be explicity set
        setAll: (cookiesToSet) => {
          cookiesToSet.forEach(({ name, value, options }) => {
            event.cookies.set(name, value, { ...options, path: '/' });
          });
        },
      },
    },
  );

  // Workaround for supabase/ssr#131: trigger token refresh early
  // before resolve() to prevent race condition with Vite 7
  await event.locals.supabase.auth.getSession();

  // Unlike `supabase.auth.getSession()`, which returns the session _without_
  // validating the JWT, this function also calls `getUser()` to validate the
  // JWT before returning the session.
  event.locals.safeGetSession = async () => {
    const {
      data: { session },
    } = await event.locals.supabase.auth.getSession();
    // no session
    if (!session) {
      return { session: null, user: null };
    }

    const {
      data: { user },
      error,
    } = await event.locals.supabase.auth.getUser();
    // no jwt token
    if (error) {
      return { session: null, user: null };
    }

    return { session, user };
  };

  return resolve(event, {
    filterSerializedResponseHeaders(name) {
      // forward supabase headers
      return name === 'content-range' || name === 'x-supabase-api-version';
    },
  });
};

@mandarini
Copy link
Contributor Author

Summoning @hf, maybe some better solution for @ixxie ?

@jackien1
Copy link

pls help

@j4w8n
Copy link
Contributor

j4w8n commented Jan 21, 2026

Can someone add more context for this issue? I've got a branch running vite 7 and async svelte, but I've not run across this issue.

@jackien1
Copy link

Can someone add more context for this issue? I've got a branch running vite 7 and async svelte, but I've not run across this issue.

Having this issue with cookies when using Social Login with callback endpoint like /auth/callback. Working if you're not using any callback endpoint and redirecting to /.

@jackien1
Copy link

Can someone add more context for this issue? I've got a branch running vite 7 and async svelte, but I've not run across this issue.

are you using callback endpoint or root redirect?

@j4w8n
Copy link
Contributor

j4w8n commented Jan 29, 2026

Can someone add more context for this issue? I've got a branch running vite 7 and async svelte, but I've not run across this issue.

are you using callback endpoint or root redirect?

Using a callback endpoint. All auth things are server side

@mandarini mandarini requested a review from hf January 29, 2026 15:15
@mandarini mandarini force-pushed the fix/async-token-refresh-race-condition branch from 9c556eb to 120b48c Compare February 16, 2026 15:45
@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request adds explicit SSR session initialization controls and related docs and tests. createServerClient now sets skipAutoInitialize, wraps auth.initialize to track a private _initialized flag, and exposes auth.isInitialized(). applyServerStorage catches setAll errors, logs a specific message when cookies are set after the response, and rethrows other errors. Documentation explains when to call initialize() (early in request handlers) and when to skip it (OAuth callbacks). Tests cover initialization behavior, concurrency safety, and the new cookie error handling.

Sequence Diagram

sequenceDiagram
    participant Handler as Request Handler
    participant Client as createServerClient
    participant Auth as auth (initialize / isInitialized)
    participant SessionAPI as getSession()
    participant Cookies as applyServerStorage.setAll
    participant Response as HTTP Response

    Handler->>Client: createServerClient(skipAutoInitialize:true)
    Handler->>Auth: auth.initialize()
    Auth->>SessionAPI: getSession()
    SessionAPI-->>Auth: session
    Auth->>Auth: set _initialized = true
    Auth-->>Handler: initialize resolved

    Handler->>Cookies: setAll(tokens)
    Cookies->>Response: cookies.set(...)
    alt response already sent -> setAll throws "after the response"
        Cookies-->>Handler: throw response-sent error
        Note right of Handler: logs helpful message about token refresh timing
    else other error
        Cookies-->>Handler: rethrow original error
    end

    Handler->>Response: send response
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: introducing configurable session initialization modes to address a race condition in async token refresh.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@mandarini mandarini force-pushed the fix/async-token-refresh-race-condition branch from 120b48c to 182343e Compare February 16, 2026 15:48
@mandarini mandarini changed the title fix: prevent race condition in async token refresh feat: prevent race condition in async token refresh Feb 16, 2026
@mandarini mandarini changed the title feat: prevent race condition in async token refresh feat: configurable session initialization to prevent race condition Feb 16, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@README.md`:
- Around line 26-86: The TS example in the README is missing a comma after the
cookies object in the createServerClient call and the fenced error block lacks a
language tag; update the snippet near createServerClient so the options object
reads `cookies: { getAll, setAll },` and add a language identifier (e.g.,
```text) to the error fence `Error: Cannot use \`cookies.set(...)\` after the
response has been generated`; after making these edits run Prettier/formatting
on README.md to clear the CI warning.

In `@src/types.ts`:
- Around line 69-76: Prettier is complaining about quote style for the
SessionInitializationMode type; update the string literals in the exported type
SessionInitializationMode in src/types.ts to use the project's preferred quotes
(e.g. change 'auto' | 'manual' to "auto" | "manual") or simply run the project's
Prettier formatter to reformat the file so the union uses the consistent quote
style while keeping the boolean false unchanged.

@mandarini mandarini requested a review from fadymak February 16, 2026 16:31
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@README.md`:
- Around line 44-46: Update the Markdown fenced code block that shows the error
so it includes a language tag (e.g., change the fence to ```text) and remove the
leading space before the error line so the block is well-formed; after that run
Prettier/formatting on README to satisfy CI. Locate the error example block
containing "Error: Cannot use `cookies.set(...)` after the response has been
generated" and apply these changes.

@mandarini mandarini force-pushed the fix/async-token-refresh-race-condition branch from 5f32054 to 7b01148 Compare February 18, 2026 11:44
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/createServerClient.spec.ts`:
- Around line 385-473: Tests are touching private internals
(supabase.auth.settings and assuming initialize() calls getSession) which makes
them brittle; update the tests that use createServerClient so they assert
observable behavior instead: for the "skipAutoInitialize" case, do not access
supabase.auth.settings — instead verify no network/global.fetch is invoked
during client creation and that auth.isInitialized() remains false until you
call (supabase.auth as any).initialize(); for the concurrency test, stop
hijacking getSession internals and either mock/createClient to capture the auth
options passed at creation time or spy/mock the global.fetch used by the auth
initializer to count actual initialization calls, then assert isInitialized()
toggles to true and that the fetch/initializer was invoked only once when
multiple initialize() calls run in parallel.

@mandarini mandarini changed the title feat: configurable session initialization to prevent race condition feat: add explicit session initialization to prevent SSR race conditions Feb 18, 2026
@mandarini mandarini force-pushed the fix/async-token-refresh-race-condition branch from cdb84f7 to 95c6e05 Compare February 18, 2026 13:36
@mandarini mandarini force-pushed the fix/async-token-refresh-race-condition branch from 95c6e05 to 6fbbbea Compare February 18, 2026 13:37
@mandarini mandarini requested a review from fadymak February 18, 2026 14:27
@mandarini mandarini changed the title feat: add explicit session initialization to prevent SSR race conditions fix: use skipAutoInitialize to prevent SSR token refresh race condition Feb 18, 2026
@mandarini mandarini force-pushed the fix/async-token-refresh-race-condition branch from c380af0 to a4f415b Compare February 18, 2026 16:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment