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
Use
@supabase/ssron both sides in server-rendered apps.createBrowserClientin the browser andcreateServerClient(withgetAllandsetAll) on the server, so the verifier is stored in cookies both can read. See Server-side auth and SSR.Exchange the code exactly once. A browser client using PKCE (the default for
createBrowserClient, orflowType: "pkce"with plain supabase-js) detects?code=and exchanges it automatically, becausedetectSessionInUrlis on by default. Don't also callexchangeCodeForSession(). 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);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" });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/healthA JSON reply means the URL is right. Use exactly that base (without
/auth/v1/health) increateClient.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.