chatboxai/chatbox · error · Error

Token refresh failed: ${error}

Error message

Token refresh failed: ${error}

What it means

Thrown when the POST to Anthropic's TOKEN_URL with grant_type=refresh_token returns non-2xx. The raw error body is interpolated, so Anthropic's reason (typically invalid_grant when the refresh token is revoked or expired) appears in the message. This terminates the refresh attempt and propagates to the caller.

Source

Thrown at src/main/oauth/providers/anthropic.ts:154

  async refreshToken(credentials) {
    if (!credentials.refreshToken) {
      throw new Error('No refresh token available')
    }

    const response = await fetch(TOKEN_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'refresh_token',
        client_id: CLIENT_ID,
        refresh_token: credentials.refreshToken,
      }),
    })

    if (!response.ok) {
      const error = await response.text()
      log.error('[OAuth:Anthropic] Token refresh failed:', error)
      throw new Error(`Token refresh 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,
    }
  },
}

View on GitHub (pinned to 81571269ad)

Solutions

  1. Read the interpolated body — invalid_grant means the refresh token is dead and a fresh interactive login is required.
  2. On invalid_grant, clear stored credentials and prompt startLogin() rather than retrying the same dead token.
  3. For transient 5xx or network errors, retry once with backoff before giving up.
  4. Serialize refresh calls per-account to avoid token-rotation races.

Example fix

// before
const creds = await provider.refreshToken(stored)

// after
try {
  const creds = await provider.refreshToken(stored)
  return creds
} catch (e) {
  if (/invalid_grant|refresh token/i.test(String(e))) {
    await clearStoredCredentials()
    return startInteractiveLogin()
  }
  throw e
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
  return await provider.refreshToken(credentials)
} catch (e) {
  const msg = String(e)
  if (/invalid_grant|revok|expired/i.test(msg)) {
  // refresh token is dead — clear and force interactive re-login
  await clearStoredCredentials()
  throw new ReauthRequiredError(msg)
  }
  if (/5\d\d|network|fetch/i.test(msg)) return await provider.refreshToken(credentials) // single retry
  throw e
}

Prevention

When it happens

Trigger: The refresh token was revoked by the user (e.g. they disconnected the app from their Anthropic account); the token expired due to inactivity; CLIENT_ID changed; concurrent refresh calls invalidated the previous token (rotation).

Common situations: Long-idle user returns after Anthropic revoked the grant; user manually revoked access in their Anthropic console; a second device refreshed first and the old refresh token was rotated; CLIENT_ID constant drifted.

Related errors


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