moeru-ai/airi · error · Error
Token exchange failed: ${response.status} ${error}
Error message
Token exchange failed: ${response.status} ${error} What it means
Thrown by exchangeCodeForTokens() when the token endpoint (OIDC_TOKEN_PATH) returns a non-2xx HTTP status during the authorization-code grant. The error string includes both the status code and the raw response body, so the upstream OAuth error (invalid_grant, invalid_client, etc.) is preserved for diagnosis. This runs only after the state check passed.
Source
Thrown at packages/stage-ui/src/libs/auth-oidc.ts:106
code_verifier: flowState.codeVerifier,
resource: SERVER_URL,
}
// Confidential clients must send the secret during token exchange.
if (params.clientSecret)
bodyParams.client_secret = params.clientSecret
const body = new URLSearchParams(bodyParams)
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) {
const error = await response.text()
throw new Error(`Token exchange failed: ${response.status} ${error}`)
}
return await response.json()
}
/**
* Refresh an access token using a refresh token (RFC 6749 S6).
* Pure function — returns new tokens without writing to any store.
*/
export async function refreshAccessToken(
clientId: string,
refreshToken: string,
clientSecret?: string,
): Promise<TokenResponse> {
const params: Record<string, string> = {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: clientId,View on GitHub (pinned to 27111382b4)
Solutions
- Read the error body in the message: 'invalid_grant' → code expired/reused or redirect_uri mismatch; 'invalid_client' → wrong client_secret/client_id; 'mismatching_code_verifier' → PKCE state lost.
- Ensure params.redirectUri in exchangeCodeForTokens is byte-identical to the redirect_uri sent in buildAuthorizationURL.
- Ensure params.clientSecret is provided for confidential clients and omitted (not wrong) for public clients.
- Confirm flowState.codeVerifier came from the same flow as the code (consumeFlowState), and that sessionStorage was not cleared mid-flow.
Defensive patterns
Strategy: try-catch
Validate before calling
// ensure params and flowState are internally consistent before the call
if (!code) throw new Error('Missing authorization code in callback')
if (params.redirectUri !== originalRedirectUri)
throw new Error('redirect_uri changed between authorize and token exchange')
if (isConfidentialClient && !params.clientSecret)
throw new Error('Confidential client missing client_secret') Type guard
null
Try / catch
try {
const tokens = await exchangeCodeForTokens(code, flowState, params, returnedState)
}
catch (err) {
if (err instanceof Error && err.message.startsWith('Token exchange failed')) {
// parse status/body: invalid_grant → restart flow; invalid_client → fix secret
}
else throw err
} Prevention
- Use the same redirect_uri at authorize and token steps.
- Persist the PKCE code_verifier in the same flow as the code (consumeFlowState).
- Provide client_secret only for confidential clients; rotate it server+client together.
When it happens
Trigger: POST to /api/auth/oauth2/token returns non-2xx. Common OAuth2 error causes: expired/already-used authorization code, redirect_uri mismatch between authorize and token requests, wrong client_id, wrong/missing client_secret for a confidential client, or a code_verifier that does not match the code_challenge sent at authorize time.
Common situations: The user took too long on the consent screen and the code expired. The redirect_uri was rewritten by a proxy so it no longer matches. PKCE verifier lost (sessionStorage cleared between authorize and callback). Client secret rotated server-side but not in the client config. Clock skew on the server rejecting the code.
Related errors
- Token exchange failed (${response.status}): ${text}
- Token refresh failed: ${response.status}
- 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/2b9f642d0f8cca3f.
Report an issue: GitHub.