moeru-ai/airi · error · Error
Token refresh failed: ${response.status}
Error message
Token refresh failed: ${response.status} What it means
Thrown by refreshAccessToken() when the token endpoint returns non-2xx during a refresh-token grant (RFC 6749 S6). Unlike the exchange error, this variant includes only the status code, not the body — callers must treat it as 'the refresh token is no longer usable'. It is a pure function; the caller decides whether to force re-authentication.
Source
Thrown at packages/stage-ui/src/libs/auth-oidc.ts:140
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: clientId,
resource: SERVER_URL,
}
if (clientSecret)
params.client_secret = clientSecret
const body = new URLSearchParams(params)
const response = await fetch(new URL(OIDC_TOKEN_PATH, SERVER_URL), {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
})
if (!response.ok)
throw new Error(`Token refresh failed: ${response.status}`)
return await response.json()
}
// Session storage keys for PKCE flow state (survives page navigation during OAuth)
const FLOW_STATE_KEY = 'auth/v1/oidc-flow-state'
const FLOW_PARAMS_KEY = 'auth/v1/oidc-flow-params'
/**
* Persist OIDC flow state before navigating to the authorization server.
*/
export function persistFlowState(flowState: OIDCFlowState, params: OIDCFlowParams): void {
sessionStorage.setItem(FLOW_STATE_KEY, JSON.stringify(flowState))
sessionStorage.setItem(FLOW_PARAMS_KEY, JSON.stringify(params))
}
/**
* Retrieve and clear persisted OIDC flow state after callback.View on GitHub (pinned to 27111382b4)
Solutions
- Treat this error as a re-authentication trigger: discard stored tokens and restart the authorization-code flow via buildAuthorizationURL().
- If using rotated refresh tokens, ensure each refresh stores the new refresh_token from the response so the next refresh uses the live token.
- Confirm client_id (and clientSecret for confidential clients) match the registered client.
- If 400 invalid_grant recurs with fresh tokens, check server refresh-token revocation policy.
Defensive patterns
Strategy: try-catch
Validate before calling
// before refreshing, confirm the token is present and not obviously expired
if (!refreshToken) throw new Error('No refresh token stored')
// let the refresh attempt proceed; the server is the source of truth Type guard
null
Try / catch
try {
return await refreshAccessToken(clientId, refreshToken, clientSecret)
}
catch (err) {
if (err instanceof Error && err.message.startsWith('Token refresh failed')) {
// refresh token unusable → clear tokens, force re-authentication via authorization code flow
}
else throw err
} Prevention
- Store the new refresh_token from each refresh response (rotated tokens).
- Treat refresh failure as a re-auth trigger, not a retry.
- Keep client_id/secret consistent with the registered client.
When it happens
Trigger: POST to /api/auth/oauth2/token with grant_type=refresh_token returns non-2xx. The refresh token was revoked or expired server-side, the client_id/client_secret changed, or the token was already rotated (one-time-use refresh tokens).
Common situations: Refresh token expired past its lifetime. User signed out / revoked access on another device, invalidating the family. Server rotated to a new refresh token on the last refresh and the old one is now dead. Client credentials changed.
Related errors
- Token exchange failed: ${response.status} ${error}
- Token exchange failed (${response.status}): ${text}
- link failed
- OIDC state mismatch — possible CSRF attack
- Auth request failed (${response.status})
AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12).
Data as JSON: /api/errors/125978890338f9a9.
Report an issue: GitHub.