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
- Read the interpolated body — invalid_grant means the refresh token is dead and a fresh interactive login is required.
- On invalid_grant, clear stored credentials and prompt startLogin() rather than retrying the same dead token.
- For transient 5xx or network errors, retry once with backoff before giving up.
- 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
- Serialize refresh calls per account to avoid token-rotation races.
- Refresh proactively before expiry (the 5-minute skew buffer helps) rather than reactively.
- Clear stored credentials on any invalid_grant to avoid hammering a dead token.
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
- Token exchange failed: ${error}
- No refresh token available
- Token refresh failed: ${text}
- Token refresh failed: ${response.status}
- Token refresh failed: ${text}
AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12).
Data as JSON: /api/errors/65edfc3ad2f63510.
Report an issue: GitHub.