decolua/9router · error · Error
`Token exchange failed: ${error}`
Error message
`Token exchange failed: ${error}` What it means
Gemini CLI OAuth token exchange: the POST trading the authorization code (plus redirect_uri) for tokens returned a non-2xx status. The body text is interpolated into this Error. Because mapTokens/postExchange never run, no Gemini credentials are stored and the login fails.
Source
Thrown at src/lib/oauth/providers/gemini-cli.js:36
exchangeToken: async (config, code, redirectUri) => {
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,
client_secret: config.clientSecret,
code: code,
redirect_uri: redirectUri,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
}
return await response.json();
},
postExchange: async (tokens) => {
// Fetch user info
const userInfoRes = await fetch(`${GEMINI_CONFIG.userInfoUrl}?alt=json`, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
const userInfo = userInfoRes.ok ? await userInfoRes.json() : {};
// Fetch project ID
let projectId = "";
try {
const projectRes = await fetch(
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
{
method: "POST",View on GitHub (pinned to 90b52e06ff)
Solutions
- Read the body's 'error' field to distinguish invalid_grant vs invalid_client vs server errors.
- Sync system clock (NTP) and restart the login flow to get a fresh code.
- Ensure the redirect_uri in the exchange matches the authorize request exactly.
- Check Google OAuth client config / Gemini CLI version for changed client credentials.
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
// Pre-exchange checks for Gemini login
if (!code) throw new Error("No authorization code for Gemini exchange");
if (Math.abs(Date.now() - (await ntpOffset())) > 90_000) throw new Error("Clock skew too large for Google OAuth — sync NTP"); Type guard
function isGoogleOAuthError(body) {
return typeof body === "object" && body !== null &&
typeof body.error === "string"; // e.g. "invalid_grant"
} Try / catch
try {
const tokens = await geminiProvider.exchangeCode(code, redirectUri);
} catch (err) {
if (String(err.message).includes("Token exchange failed")) {
if (/invalid_grant/.test(err.message)) {
console.error("Gemini code expired/replayed or clock skew — restart login");
await restartGeminiLogin();
} else {
console.error("Gemini token endpoint error:", err.message);
}
} else throw err;
} Prevention
- Keep the system clock NTP-synced; Google rejects grants under skew.
- Use an identical redirect_uri string for authorize and exchange (watch localhost ports).
- Add a timeout so a hung Google token call fails before the code expires.
- Distinguish invalid_grant (restart flow) from server errors (safe retry).
When it happens
Trigger: Calling exchangeCode for gemini-cli when Google's OAuth token endpoint responds !response.ok — invalid_grant (code expired, already used, or clock skew), invalid_client, redirect_uri mismatch, or transient 5xx/429.
Common situations: Machine clock skewed (Google rejects grants); user delayed past the code's ~10-minute life; redirect_uri differs between authorize and exchange (e.g. different port on localhost); stale/revoked OAuth client; Google outage.
Related errors
- `ClinePass token exchange failed: ${error}`
- `Token exchange failed: ${error}`
- `GitLab token exchange failed: ${await response.text()}`
- Token exchange failed: ${error}
- Token exchange failed: ${error}
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/885084b64ec86d0b.
Report an issue: GitHub.