chatboxai/chatbox · error · Error
Token exchange failed: ${error}
Error message
Token exchange failed: ${error} What it means
Thrown when the POST to Anthropic's TOKEN_URL (console.anthropic.com/v1/oauth/token) with grant_type=authorization_code returns a non-2xx status. The raw response body is interpolated into the message, so the Anthropic API's own error JSON or text surfaces directly. This is the exchange step that trades the pasted authorization code for access/refresh tokens.
Source
Thrown at src/main/oauth/providers/anthropic.ts:120
const response = await fetch(TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
code,
// Preserve the verifier-backed state on exchange as well, otherwise Anthropic rejects
// the flow even though this differs from a more typical OAuth implementation.
state: state || verifier,
redirect_uri: REDIRECT_URI,
code_verifier: verifier,
}),
})
if (!response.ok) {
const error = await response.text()
log.error('[OAuth:Anthropic] Token exchange failed:', error)
throw new Error(`Token exchange failed: ${error}`)
}
const data = (await response.json()) as {
access_token: string
refresh_token: string
expires_in: number
}
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
}
},
async refreshToken(credentials) {
if (!credentials.refreshToken) {
throw new Error('No refresh token available')View on GitHub (pinned to 81571269ad)
Solutions
- Inspect the interpolated error body — Anthropic typically returns a JSON reason (invalid_grant, invalid_client) that pinpoints the cause.
- If invalid_grant / 'code already redeemed': restart the flow via startLogin() and have the user authorize and paste a fresh code promptly.
- If invalid_client or redirect_uri mismatch: verify CLIENT_ID and REDIRECT_URI constants against the current Anthropic app registration.
- Confirm the exchange body still sends state: state || verifier and code_verifier equal to the original verifier (do not regenerate PKCE between startLogin and exchangeCode).
Example fix
// before
const error = await response.text()
log.error('[OAuth:Anthropic] Token exchange failed:', error)
throw new Error(`Token exchange failed: ${error}`)
// after
const errorBody = await response.text()
log.error('[OAuth:Anthropic] Token exchange failed:', response.status, errorBody)
const reason = parseOAuthError(errorBody)
if (response.status === 400 && reason === 'invalid_grant') {
throw new Error('Authorization code expired or already used. Please restart the login flow.')
}
throw new Error(`Token exchange failed (${response.status}): ${reason || errorBody}`) Defensive patterns
Strategy: retry
Validate before calling
// Ensure the exchange payload is internally consistent before the request.
function assertExchangePayload(p: { code: string; verifier: string; state?: string; clientId: string; redirectUri: string }) {
if (!p.code) throw new Error('code required')
if (!p.verifier) throw new Error('verifier required')
if (!p.clientId || !p.redirectUri) throw new Error('client_id and redirect_uri required')
} Type guard
function isAnthropicErrorBody(body: unknown): body is { error: string; error_description?: string } {
return typeof body === 'object' && body !== null && 'error' in body && typeof (body as any).error === 'string'
} Try / catch
try {
return await provider.exchangeCode(authInput)
} catch (e) {
const msg = String(e)
if (/invalid_grant|already|expir/i.test(msg)) {
// code dead — restart flow, do NOT retry same code
throw new ReauthRequiredError('Authorization code expired; restart login.')
}
if (/5\d\d|fetch|network/i.test(msg)) return await provider.exchangeCode(authInput) // one retry
throw e
} Prevention
- Submit the pasted code immediately after authorization — codes are short-lived.
- Never reuse a code; if the exchange fails, restart with startLogin().
- Keep state tied to the verifier as the file documents; do not decouple them.
- Verify CLIENT_ID and REDIRECT_URI constants match the Anthropic app registration after any refactor.
When it happens
Trigger: The authorization code is expired, already used once, or was generated by a different PKCE verifier; the CLIENT_ID or REDIRECT_URI constant has drifted from what Anthropic registered; the state sent on exchange differs from what the authorize page expects; network proxy rewriting the request body.
Common situations: User waits too long before pasting the code (codes are short-lived); user pastes a code from a previous login attempt; a refactor decoupled state from the verifier (the file's own comment warns this breaks the flow); clock skew or copied constants are stale after an Anthropic-side change.
Related errors
- Token exchange failed: ${response.status}
- Token refresh failed: ${error}
- MiniMax authorization failed: ${text}
- Qwen device authorization failed: ${text}
- No authorization code found in the input
AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12).
Data as JSON: /api/errors/e6453211bd568d88.
Report an issue: GitHub.