PKCE errors: code verifier, cannot parse response

Fix PKCE sign-in errors such as "code verifier not found", "both auth code and code verifier should be non-empty", flow_state_not_found and responses the client can't parse.

What you see

One of these, usually on the page that handles ?code=... after OAuth or an email link:

AuthPKCECodeVerifierMissingError: PKCE code verifier not found in storage
AuthApiError: invalid request: both auth code and code verifier should be non-empty

Or an AuthApiError with error_code set to flow_state_not_found, flow_state_expired or bad_code_verifier. Sometimes the client can't read the response at all and reports an unknown error such as "cannot parse response" or Unexpected token '<'.

Why it happens

PKCE is the sign-in flow used by @supabase/ssr and by most server-rendered apps. When sign-in starts, the client creates a secret code verifier, stores it (in a cookie with @supabase/ssr, in local storage with plain supabase-js) and sends Auth a hash of it. Auth later returns a one-time code, and exchangeCodeForSession(code) sends the code together with the stored verifier.

Error Usual cause
Code verifier not found, or "should be non-empty" The link was opened in a different browser or device (common with email links opened from a mail app), storage was cleared, or the client that started sign-in couldn't save cookies.
flow_state_not_found The code was already exchanged: a page refresh, React running an effect twice, or your code exchanging it while the client also detected it in the URL.
flow_state_expired Too long passed between starting sign-in and coming back.
bad_code_verifier The verifier in storage came from a different sign-in attempt, often from two attempts in two tabs.
Cannot parse response The request never reached Auth. The project URL is wrong (has /auth/v1 or /rest/v1 on the end, a typo, http instead of https), or a proxy returned an HTML page.

How to fix it

  1. Use @supabase/ssr on both sides in server-rendered apps. createBrowserClient in the browser and createServerClient (with getAll and setAll) on the server, so the verifier is stored in cookies both can read. See Server-side auth and SSR.

  2. Exchange the code exactly once. A browser client using PKCE (the default for createBrowserClient, or flowType: "pkce" with plain supabase-js) detects ?code= and exchanges it automatically, because detectSessionInUrl is on by default. Don't also call exchangeCodeForSession(). If you prefer to do it yourself, turn detection off:

    const supabase = createClient(import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_ANON_KEY, {
      auth: { flowType: "pkce", detectSessionInUrl: false },
    });
    
    const code = new URL(window.location.href).searchParams.get("code");
    if (code) await supabase.auth.exchangeCodeForSession(code);
    
  3. Give email links a fallback. If users often open emails on another device, let them type the one-time code that every Auth email includes:

    await supabase.auth.verifyOtp({ email, token: code, type: "email" });
    
  4. Check the project URL. It should be the bare project address, with no path:

    curl -H "apikey: $SUPABASE_ANON_KEY" https://app-one.acme.supahost.space/auth/v1/health
    

    A JSON reply means the URL is right. Use exactly that base (without /auth/v1/health) in createClient.

  5. Retry from the start. For expired or already-used flows, send the user back to the sign-in page. Retrying the same callback URL will keep failing.

On SupaHost

  • Email templates can't be switched to a link format your own server verifies (the button link is added automatically). In server-rendered apps, use PKCE with a callback route, or the one-time code.
  • Unknown paths on the project host return a JSON 404 (no Route matched with those values), so a parse error usually points to a typo in the host or a proxy in front of it.

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us