chatboxai/chatbox · error · Error
Token refresh failed: ${text}
Error message
Token refresh failed: ${text} What it means
Thrown by refreshToken() when the token endpoint responds with a non-OK status. Unlike authenticate(), this path does not parse a JSON error body — it reads the raw response text and embeds it verbatim. Refresh is called when stored credentials are near expiry and credentials.refreshToken is present (otherwise it returns the existing credentials with a warning).
Source
Thrown at src/main/oauth/providers/qwen.ts:186
}
const response = await fetch(`${QWEN_BASE_URL}/api/v1/oauth2/token`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: QWEN_CLIENT_ID,
refresh_token: credentials.refreshToken,
}).toString(),
})
if (!response.ok) {
const text = await response.text()
log.error('[OAuth:Qwen] Token refresh failed:', text)
throw new Error(`Token refresh failed: ${text}`)
}
const payload = (await response.json()) as {
access_token: string
refresh_token?: string
expires_in?: number
}
return {
accessToken: payload.access_token,
refreshToken: payload.refresh_token || credentials.refreshToken,
expiresAt: toExpiresAt(payload.expires_in),
}
},
}
View on GitHub (pinned to 81571269ad)
Solutions
- On 'invalid_grant' in the response text, clear stored Qwen credentials and re-run the full authenticate() device flow — refresh is unrecoverable.
- Verify refresh-token rotation is persisted: after a successful refresh, save payload.refresh_token (the new one) rather than credentials.refreshToken.
- If the response text is HTML/non-JSON, retry with exponential backoff — it is a transient server/proxy error, not a credential problem.
- Confirm QWEN_CLIENT_ID matches the value used during the original authenticate(); a mismatch invalidates the refresh_token.
Example fix
// before
if (!response.ok) {
const text = await response.text()
throw new Error(`Token refresh failed: ${text}`)
}
// after: distinguish recoverable vs unrecoverable
if (!response.ok) {
const text = await response.text()
if (text.includes('invalid_grant')) throw new QwenRefreshTokenRevokedError(text)
throw new Error(`Token refresh failed: ${text}`)
} Defensive patterns
Strategy: try-catch
Validate before calling
if (!credentials?.refreshToken) { return reAuthenticate() } Type guard
function isRefreshFailure(e: unknown): e is Error {
return e instanceof Error && e.message.startsWith('Token refresh failed:')
} Try / catch
try { return await qwenProvider.refreshToken(credentials) }
catch (e) {
if (isRefreshFailure(e) && /invalid_grant|expired/i.test(e.message)) { await clearQwenCredentials(); return qwenProvider.authenticate() }
throw e
} Prevention
- Persist the rotated refresh_token returned by each successful refresh (payload.refresh_token).
- Re-authenticate proactively when the refresh token is older than its expected lifetime.
- Do not reuse a refresh token after a 401 on the resource API.
When it happens
Trigger: POST to ${QWEN_BASE_URL}/api/v1/oauth2/token with grant_type=refresh_token where the refresh_token has expired or been revoked (400 invalid_grant), the client_id mismatched (401), the network/proxy returned a gateway error (502/503), or Qwen rotated the refresh token on a prior call but the caller persisted the old one.
Common situations: Long-offline client whose refresh_token exceeded Qwen's refresh lifetime; the previous refreshToken response returned a new refresh_token but the store kept the old one (see line 197 which prefers payload.refresh_token || credentials.refreshToken — a store bug nullifies rotation); clock skew; QWEN_CLIENT_ID changed between releases.
Related errors
- Qwen OAuth failed: ${payload.error_description || payload.er
- Token refresh failed: ${error}
- Token refresh failed: ${text}
- Token refresh failed: ${response.status}
- Qwen device authorization failed: ${text}
AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12).
Data as JSON: /api/errors/9ac087ec0e7c4879.
Report an issue: GitHub.