{"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/27111382b4a79a7e983289d6e983a06af185ed0f/packages/stage-ui/src/libs/auth-oidc.ts#L88-L124","documentation":"Thrown by exchangeCodeForTokens() when the token endpoint (OIDC_TOKEN_PATH) returns a non-2xx HTTP status during the authorization-code grant. The error string includes both the status code and the raw response body, so the upstream OAuth error (invalid_grant, invalid_client, etc.) is preserved for diagnosis. This runs only after the state check passed.","triggerScenarios":"POST to /api/auth/oauth2/token returns non-2xx. Common OAuth2 error causes: expired/already-used authorization code, redirect_uri mismatch between authorize and token requests, wrong client_id, wrong/missing client_secret for a confidential client, or a code_verifier that does not match the code_challenge sent at authorize time.","commonSituations":"The user took too long on the consent screen and the code expired. The redirect_uri was rewritten by a proxy so it no longer matches. PKCE verifier lost (sessionStorage cleared between authorize and callback). Client secret rotated server-side but not in the client config. Clock skew on the server rejecting the code.","solutions":["Read the error body in the message: 'invalid_grant' → code expired/reused or redirect_uri mismatch; 'invalid_client' → wrong client_secret/client_id; 'mismatching_code_verifier' → PKCE state lost.","Ensure params.redirectUri in exchangeCodeForTokens is byte-identical to the redirect_uri sent in buildAuthorizationURL.","Ensure params.clientSecret is provided for confidential clients and omitted (not wrong) for public clients.","Confirm flowState.codeVerifier came from the same flow as the code (consumeFlowState), and that sessionStorage was not cleared mid-flow."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// ensure params and flowState are internally consistent before the call\nif (!code) throw new Error('Missing authorization code in callback')\nif (params.redirectUri !== originalRedirectUri)\n  throw new Error('redirect_uri changed between authorize and token exchange')\nif (isConfidentialClient && !params.clientSecret)\n  throw new Error('Confidential client missing client_secret')","typeGuard":"null","tryCatchPattern":"try {\n  const tokens = await exchangeCodeForTokens(code, flowState, params, returnedState)\n}\ncatch (err) {\n  if (err instanceof Error && err.message.startsWith('Token exchange failed')) {\n    // parse status/body: invalid_grant → restart flow; invalid_client → fix secret\n  }\n  else throw err\n}","preventionTips":["Use the same redirect_uri at authorize and token steps.","Persist the PKCE code_verifier in the same flow as the code (consumeFlowState).","Provide client_secret only for confidential clients; rotate it server+client together."],"tags":["network","oidc","oauth","auth","token-exchange","pkce"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}