decolua/9router · error · Error
Token exchange failed: ${error}
Error message
Token exchange failed: ${error} What it means
GeminiCLIService.exchangeCode() (src/lib/oauth/services/gemini.js:55) POSTs the authorization code to Google's OAuth token endpoint (application/x-www-form-urlencoded with client_id/client_secret). When Google responds with a non-2xx status, the raw response body is wrapped into 'Token exchange failed: <body>' and thrown. The embedded body is Google's standard OAuth error JSON (e.g. {"error":"invalid_grant",...}).
Source
Thrown at src/lib/oauth/services/gemini.js:55
async exchangeCode(code, redirectUri) {
const response = await fetch(this.config.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: this.config.clientId,
client_secret: this.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();
}
/**
* Fetch project ID from Google Cloud Code Assist
*/
async fetchProjectId(accessToken) {
const response = await fetch(
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
{
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": "google-api-nodejs-client/9.15.1",
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",View on GitHub (pinned to 90b52e06ff)
Solutions
- Parse the message body's 'error' field — it names the exact OAuth failure (invalid_grant, redirect_uri_mismatch, invalid_client).
- For invalid_grant: restart the whole connect() flow from the browser authorization step; codes are single-use and short-lived.
- For redirect_uri_mismatch: ensure the identical `http://localhost:${port}/callback` used in buildAuthUrl is passed to exchangeCode.
- For invalid_client: check GEMINI_CONFIG clientId/clientSecret in src/lib/oauth/constants/oauth.js against the current Gemini CLI OAuth client values.
- Retry once on 5xx only; 4xx responses will not succeed on retry without a fresh code.
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 hint = "";
try { hint = JSON.parse(error).error || ""; } catch {}
if (hint === "invalid_grant") hint = " (code expired or already used — restart connect())";
if (hint === "redirect_uri_mismatch") hint = " (redirect_uri differs from authorize step)";
throw new Error(`Token exchange failed: ${error}${hint}`);
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-checks before exchange
if (!code) throw new Error("No authorization code to exchange");
if (!redirectUri.startsWith("http://localhost:")) throw new Error("redirect_uri must match the authorize-step localhost URI"); Type guard
function isOAuthErrorBody(text) {
try { const b = JSON.parse(text); return typeof b.error === "string"; } catch { return false; }
} Try / catch
try {
const tokens = await geminiService.exchangeCode(code, redirectUri);
} catch (err) {
const m = /^Token exchange failed: (.*)/.exec(err.message);
if (m) {
const reason = (() => { try { return JSON.parse(m[1]).error; } catch { return m[1]; } })();
if (reason === "invalid_grant") console.error("Code expired/used — restart connect() from the browser step.");
else if (reason === "redirect_uri_mismatch") console.error("Pass the exact same redirect_uri used in the authorize URL.");
else if (reason === "invalid_client") console.error("Bundled client credentials are stale — update GEMINI_CONFIG.");
else console.error("Token exchange failed:", reason);
} else throw err;
} Prevention
- Treat authorization codes as single-use, ~10-minute-lifetime secrets — exchange immediately.
- Never restart the local callback server between authorize and exchange (port changes break redirect_uri).
- Pin/verify GEMINI_CONFIG client credentials against the current Gemini CLI release.
- Only retry on 5xx; 4xx responses need a fresh authorization code.
When it happens
Trigger: Calling exchangeCode(code, redirectUri) during connect() when Google returns 4xx — 'invalid_grant' (code already used or expired, ~10 min lifetime, single-use), 'redirect_uri_mismatch' (redirectUri differs from the one used in the authorize step), 'invalid_client' (bundled GEMINI_CONFIG client credentials changed), or a 5xx from Google.
Common situations: Re-running connect() after the callback was already consumed; the local callback server restarted on a different port between authorize and exchange so the redirect_uri no longer matches; authorization code pasted late and expired; Google rotated the embedded CLI OAuth client credentials.
Related errors
- `Token exchange failed: ${error}`
- `Token exchange failed: ${error}`
- `Cline 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/8f1aa3777269dd5c.
Report an issue: GitHub.