decolua/9router · error
xAI token exchange failed: ${err}
Error message
xAI token exchange failed: ${err} What it means
exchangeXaiCode swaps an OAuth authorization code (with PKCE verifier) for tokens at xAI's token endpoint. When the HTTP response is not ok, the raw response body text is read and thrown verbatim inside 'xAI token exchange failed: <body>'. The body usually contains xAI's OAuth error JSON (e.g. invalid_grant, invalid_client).
Source
Thrown at src/lib/oauth/services/xai.js:147
async exchangeXaiCode({ tokenUrl, code, redirectUri, codeVerifier }) {
const res = await fetch(tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: XAI_CONFIG.clientId,
code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
}),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`xAI token exchange failed: ${err}`);
}
return await res.json();
}
/**
* Refresh an access token using a refresh_token.
*/
async refreshAccessToken(refreshToken) {
const { tokenUrl } = await discoverEndpoints();
const res = await fetch(tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: XAI_CONFIG.clientId,View on GitHub (pinned to 90b52e06ff)
Solutions
- Parse the thrown body text — it typically contains 'error' (e.g. invalid_grant) pinpointing the cause.
- For invalid_grant, restart the OAuth flow: authorization codes are single-use and short-lived.
- Verify client_id, redirect_uri, and the code_verifier match exactly what was used in the authorize step.
- If the exchange failed after the code was consumed, do not blindly retry — request a fresh code.
Example fix
// before
const tokens = await exchangeXaiCode(code, verifier); // throws on 400
// after
try {
const tokens = await exchangeXaiCode(code, verifier);
} catch (e) {
if (e.message.includes('invalid_grant')) return startNewAuthorization();
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
if (typeof code !== 'string' || !code || typeof codeVerifier !== 'string' || !codeVerifier) {
throw new Error('cannot exchange: missing code or verifier');
} Try / catch
try {
const tokens = await exchangeXaiCode(code, codeVerifier);
} catch (e) {
if (!e.message.startsWith('xAI token exchange failed:')) throw e;
const body = e.message.slice('xAI token exchange failed:'.length);
if (body.includes('invalid_grant')) return restartAuthorizationFlow(); // code expired/used
if (body.includes('invalid_client')) throw new Error('check XAI client_id/credentials');
throw e;
} Prevention
- Exchange the code immediately after the redirect — authorization codes are short-lived and single-use.
- Never retry an exchange that may have succeeded (a retried code always yields invalid_grant).
- Keep client_id, redirect_uri, and verifier identical between authorize and exchange calls.
- Parse the embedded upstream body to branch on the exact OAuth error code.
When it happens
Trigger: POST to the xAI token endpoint returns 400/401/etc. — expired or already-redeemed authorization code, wrong code_verifier (PKCE mismatch), invalid client_id, or revoked/blocked client.
Common situations: User taking too long between authorize and exchange (code TTL expiry); exchanging the same code twice after a retry; mismatched redirect_uri or client credentials between the authorize request and the exchange.
Related errors
- xAI token exchange failed: ${error}
- Token exchange failed: ${error}
- `Token exchange failed: ${error}`
- `Token exchange failed: ${error}`
- Token exchange failed: ${error}
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/b50084386acaa407.
Report an issue: GitHub.