chatboxai/chatbox · error · Error
Token exchange response missing access_token
Error message
Token exchange response missing access_token
What it means
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.
Source
Thrown at src/main/oauth/providers/openai.ts:155
code_verifier: verifier,
redirect_uri: REDIRECT_URI,
}),
})
if (!response.ok) {
const text = await response.text()
log.error('[OAuth:OpenAI] Token exchange failed:', text)
throw new Error(`Token exchange failed: ${response.status}`)
}
const data = (await response.json()) as {
access_token?: string
refresh_token?: string
expires_in?: number
}
if (!data.access_token) {
throw new Error('Token exchange response missing access_token')
}
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: typeof data.expires_in === 'number' ? Date.now() + data.expires_in * 1000 - 5 * 60 * 1000 : undefined,
extra: {
accountId: extractAccountId(data.access_token),
},
}
}
View on GitHub (pinned to 81571269ad)
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.
Example fix
// before
if (!data.access_token) {
throw new Error('Token exchange response missing access_token')
}
// after
if (!data.access_token) {
log.error('[OAuth:OpenAI] exchange 2xx but no access_token', JSON.stringify(data))
throw new ReauthRequiredError('OpenAI token exchange returned no access_token. Restart login.')
} Defensive patterns
Strategy: type-guard
Type guard
function isOpenAIExchangeResponse(data: unknown): data is { access_token: string; refresh_token?: string; expires_in?: number } {
return typeof data === 'object' && data !== null && typeof (data as any).access_token === 'string' && (data as any).access_token.length > 0
} Try / catch
try {
return await provider.login({ openUrl, signal })
} catch (e) {
if (/missing access_token/i.test(String(e))) {
// 2xx but malformed — captive portal or contract drift; restart login
throw new ReauthRequiredError('OpenAI exchange returned no access_token. Restart login.')
}
throw e
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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).
Related errors
- Token refresh response missing access_token
- No refresh token available
- Token exchange failed: ${response.status}
- No authorization code found in the input
- Token exchange failed: ${error}
AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12).
Data as JSON: /api/errors/b894c12b0034255f.
Report an issue: GitHub.