chatboxai/chatbox · error · Error
Token exchange failed: ${response.status}
Error message
Token exchange failed: ${response.status} What it means
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.
Source
Thrown at src/main/oauth/providers/openai.ts:145
}
async function exchangeCodeForTokens(code: string, verifier: string): Promise<OAuthCredentials> {
const response = await fetch(TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
code,
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
- 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.
Example fix
// before
const text = await response.text()
log.error('[OAuth:OpenAI] Token exchange failed:', text)
throw new Error(`Token exchange failed: ${response.status}`)
// after
const text = await response.text()
log.error('[OAuth:OpenAI] Token exchange failed:', response.status, text)
if (/invalid_grant|invalid_request/.test(text)) {
throw new ReauthRequiredError('OpenAI authorization code invalid or expired. Restart login.')
}
throw new Error(`Token exchange failed (${response.status}): ${text}`) Defensive patterns
Strategy: try-catch
Try / catch
try {
return await provider.login({ openUrl, signal })
} catch (e) {
const msg = String(e)
if (/Token exchange failed/i.test(msg)) {
if (/\b(5\d\d|network|fetch)\b/i.test(msg)) return await provider.login({ openUrl, signal }) // transient
// 400: code invalid/expired/already-used — restart login
throw new ReauthRequiredError('OpenAI authorization code invalid. Restart login.')
}
throw e
} Prevention
- 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).
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Token exchange failed: ${error}
- MiniMax authorization failed: ${text}
- Token refresh failed: ${response.status}
- Token exchange response missing access_token
- Qwen device authorization failed: ${text}
AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12).
Data as JSON: /api/errors/c1f6770632fb801e.
Report an issue: GitHub.