{"record":{"id":"b894c12b0034255f","repo":"chatboxai/chatbox","slug":"token-exchange-response-missing-access-token","errorCode":null,"errorMessage":"Token exchange response missing access_token","messagePattern":"Token exchange response missing access_token","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/oauth/providers/openai.ts","lineNumber":155,"sourceCode":"      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),\n    },\n  }\n}\n","sourceCodeStart":137,"sourceCodeEnd":167,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/main/oauth/providers/openai.ts#L137-L167","documentation":"Thrown by OpenAI's exchangeCodeForTokens() when the token endpoint returned HTTP 2xx but the JSON body has no access_token. This guards an unexpected success response — the exchange technically succeeded at the transport layer but the payload does not contain the credential the app needs.","triggerScenarios":"OpenAI changed the exchange response shape (field renamed); a 200 response body that was HTML/empty due to a captive portal or proxy; partial JSON; the account is in a state where tokens are withheld with a 200 + error field instead of a 4xx.","commonSituations":"Captive portal returning a 200 page; auth.openai.com contract drift; middleware truncating the response; the consent flow returned a 2xx but did not actually grant (rare OpenAI edge).","solutions":["Log the parsed body to detect contract drift or captive portals.","Do not proceed without an access_token — trigger login() again.","Check Content-Type before calling response.json() and treat non-JSON 200s as network/captive errors."],"exampleFix":"// before\nif (!data.access_token) {\n  throw new Error('Token exchange response missing access_token')\n}\n\n// after\nif (!data.access_token) {\n  log.error('[OAuth:OpenAI] exchange 2xx but no access_token', JSON.stringify(data))\n  throw new ReauthRequiredError('OpenAI token exchange returned no access_token. Restart login.')\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isOpenAIExchangeResponse(data: unknown): data is { access_token: string; refresh_token?: string; expires_in?: number } {\n  return typeof data === 'object' && data !== null && typeof (data as any).access_token === 'string' && (data as any).access_token.length > 0\n}","tryCatchPattern":"try {\n  return await provider.login({ openUrl, signal })\n} catch (e) {\n  if (/missing access_token/i.test(String(e))) {\n  // 2xx but malformed — captive portal or contract drift; restart login\n  throw new ReauthRequiredError('OpenAI exchange returned no access_token. Restart login.')\n  }\n  throw e\n}","preventionTips":["Check Content-Type before parsing; treat non-JSON 200s as network/captive errors.","Log the parsed body on absence to catch OpenAI contract drift early.","Never proceed without an access_token; always restart login() in that case."],"tags":["oauth","openai","token-exchange","validation","contract-drift"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}