{"record":{"id":"2b9f642d0f8cca3f","repo":"moeru-ai/airi","slug":"token-exchange-failed-response-status-error","errorCode":null,"errorMessage":"Token exchange failed: ${response.status} ${error}","messagePattern":"Token exchange failed: (.+?) (.+?)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/libs/auth-oidc.ts","lineNumber":106,"sourceCode":"    code_verifier: flowState.codeVerifier,\n    resource: SERVER_URL,\n  }\n\n  // Confidential clients must send the secret during token exchange.\n  if (params.clientSecret)\n    bodyParams.client_secret = params.clientSecret\n\n  const body = new URLSearchParams(bodyParams)\n\n  const response = await fetch(new URL(OIDC_TOKEN_PATH, SERVER_URL), {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n    body,\n  })\n\n  if (!response.ok) {\n    const error = await response.text()\n    throw new Error(`Token exchange failed: ${response.status} ${error}`)\n  }\n\n  return await response.json()\n}\n\n/**\n * Refresh an access token using a refresh token (RFC 6749 S6).\n * Pure function — returns new tokens without writing to any store.\n */\nexport async function refreshAccessToken(\n  clientId: string,\n  refreshToken: string,\n  clientSecret?: string,\n): Promise<TokenResponse> {\n  const params: Record<string, string> = {\n    grant_type: 'refresh_token',\n    refresh_token: refreshToken,\n    client_id: clientId,","sourceCodeStart":88,"sourceCodeEnd":124,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/libs/auth-oidc.ts#L88-L124","documentation":"The token endpoint POST (authorization_code grant) returned a non-2xx status; the message embeds the HTTP status and the raw response text, which per RFC 6749 S5.2 contains an error code such as invalid_grant, invalid_client, or invalid_request. This is the exchange step of the code flow, reached only after the state check passed.","triggerScenarios":"Authorization code already redeemed or expired (about 60 s lifetime) — invalid_grant; redirect_uri differing from the registered one; wrong or missing clientSecret for a confidential client; PKCE code_verifier not matching the challenge sent at authorize time; wrong client_id.","commonSituations":"A callback handler firing twice and exchanging the same code (React StrictMode double effects); dev server restart between authorize and callback; env typo in the client secret; a proxy stripping the POST body; registered redirect URI with a trailing-slash mismatch.","solutions":["Read the error code inside the response text: invalid_grant means restart the flow with a fresh code","invalid_client means fix the client_secret / env values sent in bodyParams","Verify the redirect_uri registered on the server matches params.redirectUri byte for byte","Guard against double exchange — mark the code consumed before the POST","Ensure the code_verifier is the same one used to build the authorize challenge"],"exampleFix":"// before\nwindow.addEventListener('load', () => exchange(code))\n// a second handler invocation exchanges the same code again → invalid_grant\n\n// after\nlet exchanged = false\nwindow.addEventListener('load', () => {\n  if (exchanged) return\n  exchanged = true\n  exchange(code)\n})","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const tokens = await exchangeCodeForTokens(code, flowState, params, urlState)\n}\ncatch (err) {\n  const [, , body] = err.message.match(/Token exchange failed: (\\d+) (.*)/) ?? []\n  const oauthError = body ? JSON.parse(body).error : undefined\n  if (oauthError === 'invalid_grant') restartLogin()\n  else if (oauthError === 'invalid_client') throw new Error('Check client secret configuration')\n}","preventionTips":["Exchange the code exactly once, immediately after the redirect","Keep code_verifier stored alongside state","Register redirect URIs exactly — scheme, host, path, no trailing slash","Never log or reuse authorization codes"],"tags":["oidc","oauth","token-exchange","http","pkce"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}