{"record":{"id":"8930c33f97ad56dc","repo":"decolua/9router","slug":"token-exchange-failed-error-8930c3","errorCode":null,"errorMessage":"`Token exchange failed: ${error}`","messagePattern":"`Token exchange failed: (.+?)`","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/providers/codex.js","lineNumber":43,"sourceCode":"  exchangeToken: async (config, code, redirectUri, codeVerifier) => {\n    const response = await fetch(config.tokenUrl, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        Accept: \"application/json\",\n      },\n      body: new URLSearchParams({\n        grant_type: \"authorization_code\",\n        client_id: config.clientId,\n        code: code,\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    const info = extractCodexAccountInfo(tokens.id_token);\n    const mapped = {\n      accessToken: tokens.access_token,\n      refreshToken: tokens.refresh_token,\n      idToken: tokens.id_token,\n      expiresIn: tokens.expires_in,\n      lastRefreshAt: new Date().toISOString(),\n    };\n    const email = info.email || extractEmailFromAccessToken(tokens.access_token);\n    if (email) mapped.email = email;\n    if (info.chatgptAccountId || info.chatgptPlanType) {\n      mapped.providerSpecificData = {\n        chatgptAccountId: info.chatgptAccountId,","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/providers/codex.js#L25-L61","documentation":"Codex (OpenAI) OAuth token exchange: the POST to the token endpoint with the authorization code + code_verifier (PKCE) returned a non-2xx status. The response body is read as text and rethrown, so the message contains the server's OAuth error JSON (e.g. {\"error\":\"invalid_grant\"}). The login cannot complete and no tokens are returned.","triggerScenarios":"Calling exchangeCode for codex when the token endpoint replies !response.ok — expired/replayed authorization_code, wrong or mismatched code_verifier, invalid client_id, redirect_uri mismatch, or 429/5xx.","commonSituations":"PKCE verifier/store mismatch after restarting the flow; taking longer than the code lifetime; replaying a callback URL after a browser refresh; client_id/redirect changed upstream; OpenAI-side outage.","solutions":["Parse the body — the 'error' field (invalid_grant, invalid_client, invalid_request) pinpoints the cause.","Restart the full OAuth flow to mint a new code and use the matching code_verifier from the same session.","Verify client_id and redirect_uri in config match the registered Codex OAuth app exactly.","If 429/5xx, wait and retry the exchange with the same (still-valid) code once."],"exampleFix":"// before\nif (!response.ok) {\n  const error = await response.text();\n  throw new Error(`Token exchange failed: ${error}`);\n}\n// after\nif (!response.ok) {\n  const error = await response.text();\n  let code = \"unknown\";\n  try { code = JSON.parse(error).error; } catch {}\n  throw new Error(`Token exchange failed (HTTP ${response.status}, ${code}): ${error}`);\n}","handlingStrategy":"try-catch","validationCode":"// Before exchanging, confirm PKCE state is intact and unexpired\nif (!code || !codeVerifier) throw new Error(\"Missing code or code_verifier for Codex exchange\");\nif (Date.now() - flowStartedAt > 9 * 60 * 1000) throw new Error(\"Authorization code likely expired — restart flow\");","typeGuard":"function isOAuthTokenError(body) {\n  if (typeof body !== \"object\" || body === null) return false;\n  return typeof body.error === \"string\"; // RFC 6749 §5.2\n}","tryCatchPattern":"try {\n  const tokens = await codexProvider.exchangeCode(code, codeVerifier);\n} catch (err) {\n  if (String(err.message).includes(\"Token exchange failed\")) {\n    if (/invalid_grant/.test(err.message)) {\n      // code expired/replayed/verifier mismatch — restart the whole flow\n      await restartCodexOAuthFlow();\n    } else if (/invalid_client/.test(err.message)) {\n      console.error(\"Codex client_id/secret misconfigured\");\n    } else {\n      // 429/5xx — one safe retry with the same code\n      await retryOnce(() => codexProvider.exchangeCode(code, codeVerifier));\n    }\n  } else throw err;\n}","preventionTips":["Persist code_verifier with the flow so restarts don't mismatch PKCE pairs.","Complete the exchange promptly — codes expire within minutes.","Never replay a code after a browser refresh; regenerate instead.","Parse the RFC 6749 error field from the body for precise branching."],"tags":["oauth","pkce","token-exchange","http-error"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}