{"record":{"id":"92819966dc6a97e5","repo":"moeru-ai/airi","slug":"token-exchange-failed-response-status-text","errorCode":null,"errorMessage":"Token exchange failed (${response.status}): ${text}","messagePattern":"Token exchange failed \\((.+?)\\): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/stage-tamagotchi/src/main/services/airi/auth.ts","lineNumber":156,"sourceCode":"async function exchangeCode(code: string, codeVerifier: string, redirectUri: string): Promise<TokenExchangeResult> {\n  const body = new URLSearchParams({\n    grant_type: 'authorization_code',\n    code,\n    redirect_uri: redirectUri,\n    client_id: OIDC_CLIENT_ID,\n    code_verifier: codeVerifier,\n    resource: SERVER_URL,\n  })\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 text = await response.text()\n    throw new Error(`Token exchange failed (${response.status}): ${text}`)\n  }\n\n  const data = await response.json() as Record<string, unknown>\n  return {\n    accessToken: data.access_token as string,\n    refreshToken: data.refresh_token as string | undefined,\n    idToken: data.id_token as string | undefined,\n    expiresIn: data.expires_in as number,\n  }\n}\n","sourceCodeStart":138,"sourceCodeEnd":167,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/apps/stage-tamagotchi/src/main/services/airi/auth.ts#L138-L167","documentation":"Thrown by exchangeCode during the OIDC authorization-code → token swap when the token endpoint (POST {SERVER_URL}/api/auth/oauth2/token) returns a non-2xx response. The HTTP status and raw response body are interpolated into the message, so the upstream OIDC provider's error detail is preserved.","triggerScenarios":"Posting the PKCE token exchange after the browser redirect and receiving 4xx/5xx: invalid/expired/reused authorization code, wrong redirect_uri, PKCE code_verifier mismatch, wrong client_id, expired or tampered state, network proxy returning an error page, or the auth backend (SERVER_URL / VITE_SERVER_URL) being unreachable/misconfigured.","commonSituations":"Clock skew between Electron and the auth server; redirect URI in the token request not matching the one used at authorize; user took too long and the auth code expired; auth service redeployed with new client_id; VITE_SERVER_URL points at the wrong environment; a corporate proxy intercepted the POST.","solutions":["Inspect the interpolated status and body: 400 with 'invalid_grant' usually means an expired/reused code or PKCE mismatch; 401 typically means bad client_id; 5xx means an upstream auth-service issue.","Ensure SERVER_URL (VITE_SERVER_URL) and OIDC_CLIENT_ID (VITE_OIDC_CLIENT_ID) match the deployed auth service.","Make sure the redirect_uri passed to exchangeCode is byte-identical to the one sent to the authorize endpoint.","Retry the full sign-in flow from scratch so a fresh code/verifier/state are generated.","Check the auth service logs at server/apps/auth for the matching token request."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await exchangeCode(code, codeVerifier, redirectUri)\n}\ncatch (error) {\n  const msg = errorMessageFrom(error) ?? ''\n  if (msg.startsWith('Token exchange failed')) {\n    const [, status, body] = msg.match(/\\((\\d+)\\):\\s([\\s\\S]*)$/) ?? []\n    // branch on status: 400 invalid_grant -> restart flow; 401 -> check client_id; 5xx -> retry/backoff\n  }\n  throw error\n}","preventionTips":["Keep redirect_uri identical across authorize and token calls.","Match VITE_SERVER_URL and VITE_OIDC_CLIENT_ID to the deployed auth service.","Complete the redirect promptly so the auth code does not expire.","Synchronize system clocks to avoid token endpoint time-window rejects."],"tags":["oidc","auth","network","electron","pkce"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}