decolua/9router · error · Error
`Token exchange failed: ${error}`
Error message
`Token exchange failed: ${error}` What it means
Codex (OpenAI) OAuth token exchange: the POST to the token endpoint with the authorization code + code_verifier (PKCE) returned a non-2xx status. The response body is read as text and rethrown, so the message contains the server's OAuth error JSON (e.g. {"error":"invalid_grant"}). The login cannot complete and no tokens are returned.
Source
Thrown at src/lib/oauth/providers/codex.js:43
exchangeToken: async (config, code, redirectUri, codeVerifier) => {
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: config.clientId,
code: code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
}
return await response.json();
},
mapTokens: (tokens) => {
const info = extractCodexAccountInfo(tokens.id_token);
const mapped = {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
idToken: tokens.id_token,
expiresIn: tokens.expires_in,
lastRefreshAt: new Date().toISOString(),
};
const email = info.email || extractEmailFromAccessToken(tokens.access_token);
if (email) mapped.email = email;
if (info.chatgptAccountId || info.chatgptPlanType) {
mapped.providerSpecificData = {
chatgptAccountId: info.chatgptAccountId,View on GitHub (pinned to 90b52e06ff)
Solutions
- Parse the body — the 'error' field (invalid_grant, invalid_client, invalid_request) pinpoints the cause.
- Restart the full OAuth flow to mint a new code and use the matching code_verifier from the same session.
- Verify client_id and redirect_uri in config match the registered Codex OAuth app exactly.
- If 429/5xx, wait and retry the exchange with the same (still-valid) code once.
Example fix
// before
if (!response.ok) {
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
}
// after
if (!response.ok) {
const error = await response.text();
let code = "unknown";
try { code = JSON.parse(error).error; } catch {}
throw new Error(`Token exchange failed (HTTP ${response.status}, ${code}): ${error}`);
} Defensive patterns
Strategy: try-catch
Validate before calling
// Before exchanging, confirm PKCE state is intact and unexpired
if (!code || !codeVerifier) throw new Error("Missing code or code_verifier for Codex exchange");
if (Date.now() - flowStartedAt > 9 * 60 * 1000) throw new Error("Authorization code likely expired — restart flow"); Type guard
function isOAuthTokenError(body) {
if (typeof body !== "object" || body === null) return false;
return typeof body.error === "string"; // RFC 6749 §5.2
} Try / catch
try {
const tokens = await codexProvider.exchangeCode(code, codeVerifier);
} catch (err) {
if (String(err.message).includes("Token exchange failed")) {
if (/invalid_grant/.test(err.message)) {
// code expired/replayed/verifier mismatch — restart the whole flow
await restartCodexOAuthFlow();
} else if (/invalid_client/.test(err.message)) {
console.error("Codex client_id/secret misconfigured");
} else {
// 429/5xx — one safe retry with the same code
await retryOnce(() => codexProvider.exchangeCode(code, codeVerifier));
}
} else throw err;
} Prevention
- Persist code_verifier with the flow so restarts don't mismatch PKCE pairs.
- Complete the exchange promptly — codes expire within minutes.
- Never replay a code after a browser refresh; regenerate instead.
- Parse the RFC 6749 error field from the body for precise branching.
When it happens
Trigger: Calling exchangeCode for codex when the token endpoint replies !response.ok — expired/replayed authorization_code, wrong or mismatched code_verifier, invalid client_id, redirect_uri mismatch, or 429/5xx.
Common situations: PKCE verifier/store mismatch after restarting the flow; taking longer than the code lifetime; replaying a callback URL after a browser refresh; client_id/redirect changed upstream; OpenAI-side outage.
Related errors
- `Token exchange failed: ${error}`
- `ClinePass token exchange failed: ${error}`
- `Token exchange failed: ${error}`
- `GitLab token exchange failed: ${await response.text()}`
- xAI token exchange failed: ${error}
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/8930c33f97ad56dc.
Report an issue: GitHub.