decolua/9router · error
Token exchange failed: ${error}
Error message
Token exchange failed: ${error} What it means
Thrown by OAuthService.exchangeCode() when the POST to the provider's token endpoint returns a non-2xx response. The response body text (which usually contains the OAuth error JSON such as `invalid_grant`, `invalid_client`, or `redirect_uri_mismatch`) is embedded verbatim in the message. It means the authorization code could not be swapped for tokens.
Source
Thrown at src/lib/oauth/services/oauth.js:116
grant_type: "authorization_code",
client_id: this.config.clientId,
code: code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
});
const response = await fetch(this.config.tokenUrl, {
method: "POST",
headers: {
"Content-Type": contentType,
Accept: "application/json",
},
body: body,
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
}
return await response.json();
}
/**
* Complete OAuth flow
*/
async authenticate(providerName, buildAuthUrlFn) {
// Generate PKCE
const { codeVerifier, codeChallenge, state } = generatePKCE();
// Start local server and get redirect URI
const { redirectUri, waitForCallback } = await this.startAuthFlow(null, providerName);
// Build authorization URL
const authUrl = buildAuthUrlFn(redirectUri, state, codeChallenge);
View on GitHub (pinned to 90b52e06ff)
Solutions
- Read the embedded body text — it names the exact OAuth error (invalid_grant, invalid_client, redirect_uri_mismatch) and fix that specific cause.
- Restart the whole auth flow to get a fresh, unused authorization code; codes are single-use and short-lived.
- Ensure the redirect_uri passed to exchangeCode is byte-identical to the one used in the authorize URL (same port).
- Verify clientId and PKCE code_verifier match the challenge sent in buildAuthUrl; if regenerating, regenerate both together.
- Try the alternate contentType (application/json vs application/x-www-form-urlencoded) if the provider's token endpoint rejects the default.
Example fix
// before (opaque text blob)
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
// after (surface status + parsed OAuth error)
const text = await response.text();
let detail = text;
try { detail = JSON.parse(text).error_description || JSON.parse(text).error || text; } catch {}
throw new Error(`Token exchange failed (HTTP ${response.status}): ${detail}`); Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-flight sanity checks before calling exchangeCode:
if (!code || !codeVerifier || !redirectUri) throw new Error("Missing code/verifier/redirectUri before exchange");
if (!tokenUrl.startsWith("https://")) throw new Error("Token URL must be https"); Type guard
function isTokenResponse(json) {
return json != null && typeof json === "object" && typeof json.access_token === "string";
} Try / catch
try {
const tokens = await service.exchangeCode(code, redirectUri, codeVerifier);
} catch (err) {
if (err.message.startsWith("Token exchange failed:")) {
if (err.message.includes("invalid_grant")) { /* code expired/used — rerun auth */ }
else if (err.message.includes("redirect_uri")) { /* align redirect_uri with authorize call */ }
else throw err;
} else throw err;
} Prevention
- Exchange the code immediately after receiving it — codes are single-use and expire in minutes.
- Never restart the CLI between authorize and exchange if it rebinds a different localhost port.
- Keep PKCE verifier/challenge generation and use inside a single flow instance.
- Match the Content-Type the provider's token endpoint requires (form-urlencoded vs JSON).
- Check provider status pages / network egress before attributing failures to your code.
When it happens
Trigger: exchangeCode(code, redirectUri, codeVerifier, contentType) posts grant_type=authorization_code to config.tokenUrl and the upstream replies !response.ok — e.g. the code was already redeemed or expired, code_verifier doesn't match the challenge, redirect_uri differs from the authorize request, clientId is wrong, or the Content-Type the provider expects (form vs JSON) was mismatched.
Common situations: Re-running the flow and reusing the old one-time code; PKCE verifier/challenge mismatch across restarted CLI runs; redirect_uri changed because the local callback server got a different port; provider requires HTTP Basic client auth instead of body credentials; proxy/firewall returning an HTML error page.
Related errors
- xAI token exchange failed: ${err}
- `Token exchange failed: ${error}`
- `Token exchange failed: ${error}`
- xAI token exchange failed: ${error}
- Token exchange failed: ${error}
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/fb1a4e52dac5aa4a.
Report an issue: GitHub.