{"record":{"id":"e770bd82ec648761","repo":"chatboxai/chatbox","slug":"token-refresh-response-missing-access-token","errorCode":null,"errorMessage":"Token refresh response missing access_token","messagePattern":"Token refresh response missing access_token","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/oauth/providers/openai.ts","lineNumber":115,"sourceCode":"        refresh_token: credentials.refreshToken,\n        client_id: CLIENT_ID,\n      }),\n    })\n\n    if (!response.ok) {\n      const text = await response.text()\n      log.error('[OAuth:OpenAI] Token refresh failed:', text)\n      throw new Error(`Token refresh 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 refresh response missing access_token')\n    }\n\n    return {\n      accessToken: data.access_token,\n      refreshToken: data.refresh_token || credentials.refreshToken,\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}\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({","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/main/oauth/providers/openai.ts#L97-L133","documentation":"Thrown by OpenAI's refreshToken() when the refresh POST returned HTTP 2xx but the JSON body has no access_token field. This guards a malformed/unexpected success response: OpenAI's contract requires access_token on refresh, so its absence signals an API contract change, a non-JSON body parsed loosely, or a partial response.","triggerScenarios":"OpenAI changed the response shape (field renamed); the body was HTML or empty (e.g. captive portal returning 200); a proxy stripped the body; the response was a partial JSON object missing access_token.","commonSituations":"Captive portal returning a 200 login page; OpenAI contract drift after an auth endpoint update; middleware corrupting the body; race where response.json() parsed an incomplete stream.","solutions":["Log the full response body when access_token is absent to detect contract drift or captive portals.","Treat as a hard failure — do not return credentials with a missing access_token; trigger an interactive login().","If the cause is a captive portal, detect non-JSON Content-Type before parsing and surface a network error instead."],"exampleFix":"// before\nconst data = (await response.json()) as { access_token?: string; ... }\nif (!data.access_token) {\n  throw new Error('Token refresh response missing access_token')\n}\n\n// after\nconst data = (await response.json()) as { access_token?: string; ... }\nif (!data.access_token) {\n  log.error('[OAuth:OpenAI] refresh 2xx but no access_token', JSON.stringify(data))\n  throw new ReauthRequiredError('OpenAI refresh response malformed — re-login required')\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isOpenAIRefreshResponse(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.refreshToken(credentials)\n} catch (e) {\n  if (/missing access_token/i.test(String(e))) {\n  // 2xx but malformed — likely captive portal or contract drift; re-login\n  await clearStoredCredentials()\n  throw new ReauthRequiredError('OpenAI refresh response malformed — re-login required')\n  }\n  throw e\n}","preventionTips":["Check Content-Type is JSON before parsing; treat non-JSON 200s (captive portals) as network errors.","Log the parsed body when access_token is absent to detect contract drift quickly.","Never return credentials with an undefined access_token — always re-login."],"tags":["oauth","openai","refresh-token","validation","contract-drift"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}