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

  1. On 400 with invalid_grant, restart login() — the code is single-use and short-lived.
  2. Ensure the verifier passed to exchangeCodeForTokens is the exact one used to build code_challenge in login() (do not regenerate PKCE mid-flow).
  3. Confirm redirect_uri is byte-identical on authorize and exchange.
  4. 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

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


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