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

  1. Inspect the interpolated error body — Anthropic typically returns a JSON reason (invalid_grant, invalid_client) that pinpoints the cause.
  2. If invalid_grant / 'code already redeemed': restart the flow via startLogin() and have the user authorize and paste a fresh code promptly.
  3. If invalid_client or redirect_uri mismatch: verify CLIENT_ID and REDIRECT_URI constants against the current Anthropic app registration.
  4. 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

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


AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12). Data as JSON: /api/errors/e6453211bd568d88. Report an issue: GitHub.