{"record":{"id":"3f72ea3c163ac3b8","repo":"decolua/9router","slug":"token-exchange-failed-error-3f72ea","errorCode":null,"errorMessage":"`Token exchange failed: ${error}`","messagePattern":"`Token exchange failed: (.+?)`","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/providers/claude.js","lineNumber":47,"sourceCode":"    const response = await fetch(config.tokenUrl, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Accept: \"application/json\",\n      },\n      body: JSON.stringify({\n        code: authCode,\n        state: codeState || state,\n        grant_type: \"authorization_code\",\n        client_id: config.clientId,\n        redirect_uri: redirectUri,\n        code_verifier: codeVerifier,\n      }),\n    });\n\n    if (!response.ok) {\n      const error = await response.text();\n      throw new Error(`Token exchange failed: ${error}`);\n    }\n\n    return await response.json();\n  },\n  mapTokens: (tokens) => ({\n    accessToken: tokens.access_token,\n    refreshToken: tokens.refresh_token,\n    expiresIn: tokens.expires_in,\n    scope: tokens.scope,\n  }),\n};\n\nexport default claude;\n","sourceCodeStart":29,"sourceCodeEnd":61,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/providers/claude.js#L29-L61","documentation":"claude.js exchangeToken performs the PKCE authorization-code exchange with Anthropic's token endpoint; any non-ok response triggers this error carrying the raw response body. Common OAuth failure codes (invalid_grant, invalid_client, redirect_uri_mismatch) plus state/verifier mismatches surface here.","triggerScenarios":"Exchanging a code whose PKCE code_verifier doesn't match the challenge used in buildAuthUrl, replaying a consumed/expired code, passing the code including the '#state' suffix (it is split, but a state mismatch still fails server-side), wrong clientId in CLAUDE_CONFIG, or redirect_uri differing between authorize and exchange.","commonSituations":"Anthropic rotating its OAuth client config, users completing the flow twice in two tabs, code pasted from a browser where the fragment was truncated, clock skew expiring the code, or CI environments where the callback port differs from the auth URL.","solutions":["Inspect the appended body for the OAuth error code and address it directly (invalid_grant -> restart flow).","invalid_grant: codes are single-use and short-lived; restart the login from buildAuthUrl.","Ensure the same codeVerifier generated for the auth URL is passed to exchangeToken (don't regenerate between steps).","Match redirect_uri exactly between buildAuthUrl and exchangeToken, including port and scheme.","If 5xx, retry the full flow after a short wait."],"exampleFix":"// before: fresh verifier at exchange time kills PKCE\nconst verifier = generateVerifier(); // different from the one in the auth URL\nawait exchangeToken(config, code, redirectUri, verifier, state);\n// after: persist the original verifier\nawait exchangeToken(config, code, redirectUri, pkce.verifier, state);","handlingStrategy":"try-catch","validationCode":"if (!code || !codeVerifier || !redirectUri) throw new Error('PKCE exchange requires code, codeVerifier and redirectUri');\n// code may carry '#state' — that is handled internally, do not pre-strip differently","typeGuard":"function isPkceExchangeInput(v) {\n  return typeof v?.code === 'string' && v.code.length > 0 &&\n         typeof v?.codeVerifier === 'string' && v.codeVerifier.length >= 43 &&\n         typeof v?.redirectUri === 'string' && /^https?:\\/\\//.test(v.redirectUri);\n}","tryCatchPattern":"try {\n  const tokens = await claude.exchangeToken(config, code, redirectUri, pkce.verifier, state);\n  // use tokens\n} catch (err) {\n  if (String(err.message).startsWith('Token exchange failed:')) {\n    if (/invalid_grant/.test(err.message)) restartLogin();       // replayed/expired code or verifier mismatch\n    else if (/invalid_client/.test(err.message)) updateClaudeConfig();\n    else if (/redirect_uri_mismatch/.test(err.message)) alignRedirectUri();\n    else backoffAndRetry();\n  } else throw err;\n}","preventionTips":["Persist the exact PKCE verifier produced for the auth URL and pass that same instance to exchangeToken.","Reuse one redirectUri object/value across buildAuthUrl and exchangeToken.","Never run two OAuth flows in parallel tabs for the same account.","Keep CLAUDE_CONFIG.clientId current with Anthropic's published OAuth client values.","On failure, read the embedded response body for the OAuth error code before retrying."],"tags":["oauth","pkce","token-exchange","claude","http-4xx"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}