{"record":{"id":"c1f6770632fb801e","repo":"chatboxai/chatbox","slug":"token-exchange-failed-response-status","errorCode":null,"errorMessage":"Token exchange failed: ${response.status}","messagePattern":"Token exchange failed: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/oauth/providers/openai.ts","lineNumber":145,"sourceCode":"}\n\nasync function exchangeCodeForTokens(code: string, verifier: string): Promise<OAuthCredentials> {\n  const response = await fetch(TOKEN_URL, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n    body: new URLSearchParams({\n      grant_type: 'authorization_code',\n      client_id: CLIENT_ID,\n      code,\n      code_verifier: verifier,\n      redirect_uri: REDIRECT_URI,\n    }),\n  })\n\n  if (!response.ok) {\n    const text = await response.text()\n    log.error('[OAuth:OpenAI] Token exchange failed:', text)\n    throw new Error(`Token exchange failed: ${response.status}`)\n  }\n\n  const data = (await response.json()) as {\n    access_token?: string\n    refresh_token?: string\n    expires_in?: number\n  }\n\n  if (!data.access_token) {\n    throw new Error('Token exchange response missing access_token')\n  }\n\n  return {\n    accessToken: data.access_token,\n    refreshToken: data.refresh_token,\n    expiresAt: typeof data.expires_in === 'number' ? Date.now() + data.expires_in * 1000 - 5 * 60 * 1000 : undefined,\n    extra: {\n      accountId: extractAccountId(data.access_token),","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/main/oauth/providers/openai.ts#L127-L163","documentation":"Thrown by OpenAI's exchangeCodeForTokens() when the POST to auth.openai.com/oauth/token with grant_type=authorization_code returns non-2xx. Like the refresh error, only response.status is interpolated (the body is logged but not in the message). This is the step that converts the authorization code (received via the localhost callback) into tokens.","triggerScenarios":"Authorization code expired or already used; PKCE code_verifier does not match the code_challenge sent at authorize time; redirect_uri on exchange differs from REDIRECT_URI sent at authorize; CLIENT_ID mismatch; code rejected because the callback state check was bypassed.","commonSituations":"User refreshes the callback URL (re-using the code); verifier regenerated between login() and exchangeCodeForTokens(); redirect_uri constant changed but the running authorize URL used the old one; clock skew causing code expiry.","solutions":["On 400 with invalid_grant, restart login() — the code is single-use and short-lived.","Ensure the verifier passed to exchangeCodeForTokens is the exact one used to build code_challenge in login() (do not regenerate PKCE mid-flow).","Confirm redirect_uri is byte-identical on authorize and exchange.","Improve the thrown error to include the logged body so callers can branch on the reason."],"exampleFix":"// before\nconst text = await response.text()\nlog.error('[OAuth:OpenAI] Token exchange failed:', text)\nthrow new Error(`Token exchange failed: ${response.status}`)\n\n// after\nconst text = await response.text()\nlog.error('[OAuth:OpenAI] Token exchange failed:', response.status, text)\nif (/invalid_grant|invalid_request/.test(text)) {\n  throw new ReauthRequiredError('OpenAI authorization code invalid or expired. Restart login.')\n}\nthrow new Error(`Token exchange failed (${response.status}): ${text}`)","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  return await provider.login({ openUrl, signal })\n} catch (e) {\n  const msg = String(e)\n  if (/Token exchange failed/i.test(msg)) {\n  if (/\\b(5\\d\\d|network|fetch)\\b/i.test(msg)) return await provider.login({ openUrl, signal }) // transient\n  // 400: code invalid/expired/already-used — restart login\n  throw new ReauthRequiredError('OpenAI authorization code invalid. Restart login.')\n  }\n  throw e\n}","preventionTips":["Use the verifier from login() verbatim in exchangeCodeForTokens — never regenerate PKCE mid-flow.","Keep redirect_uri byte-identical on authorize and exchange.","Improve the thrown error to include the body (it is already read into `text`).","Do not let the browser re-POST the callback URL (codes are single-use)."],"tags":["oauth","network","openai","token-exchange","pkce"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}