slopus/happy · error
Token exchange failed
Error message
Token exchange failed
What it means
In the Gemini flow, once `code` and `state` validate, exchangeCodeForTokens() POSTs the code plus PKCE verifier, client_id, and client_secret to Google's token endpoint. Any throw inside this try block — a non-2xx token response ('Token exchange failed: <body>'), a network failure, or a JSON parse error — is caught, the browser receives HTTP 500 'Token exchange failed', and the promise rejects with the original error.
Source
Thrown at packages/happy-cli/src/commands/connect/authenticateGemini.ts:172
server.close();
reject(new Error('No authorization code received'));
return;
}
try {
// Exchange code for tokens
const tokens = await exchangeCodeForTokens(code, verifier, port);
// Redirect to success page
res.writeHead(302, {
'Location': 'https://developers.google.com/gemini-code-assist/auth_success_gemini'
});
res.end();
server.close();
resolve(tokens);
} catch (error) {
res.writeHead(500);
res.end('Token exchange failed');
server.close();
reject(error);
}
}
});
server.listen(port, '127.0.0.1', () => {
// console.log(`🔐 OAuth callback server listening on port ${port}`);
});
// Timeout after 5 minutes
setTimeout(() => {
server.close();
reject(new Error('Authentication timeout'));
}, 5 * 60 * 1000);
});
}View on GitHub (pinned to b824cd0a46)
Solutions
- Rerun the connect flow for a fresh code and verifier; do not retry with the same code or reload the callback page.
- Check the logged token-endpoint error body for the exact Google error code (invalid_grant, invalid_client, redirect_uri_mismatch).
- Verify network access to oauth2.googleapis.com (VPN/proxy/firewall) and correct system time.
- Ensure the callback port is stable between the authorize and token requests (avoid other processes claiming the port mid-flow); update happy-cli if issues persist.
Example fix
// before: reloading the callback page replays a spent code → 500 Token exchange failed
// after: always start a new flow on failure
try {
const tokens = await authenticateGemini();
} catch (e) {
// rerun authenticateGemini() — fresh code, verifier, and state
} Defensive patterns
Strategy: retry
Validate before calling
// Confirm Google's token endpoint is reachable before the OAuth flow
const reachable = await fetch('https://oauth2.googleapis.com').then(r => r.status < 500).catch(() => false);
if (!reachable) throw new Error('oauth2.googleapis.com unreachable — check network/proxy before authenticating'); Try / catch
async function connectGeminiWithRetry(maxAttempts = 2): Promise<GeminiAuthTokens> {
let lastErr: unknown;
for (let i = 0; i < maxAttempts; i++) {
try {
return await authenticateGemini();
} catch (err) {
lastErr = err;
const msg = err instanceof Error ? err.message : String(err);
if (/Token exchange failed|invalid_grant/i.test(msg)) continue; // restart flow with a fresh code
throw err;
}
}
throw lastErr;
} Prevention
- Do not refresh the callback page; a replayed code fails the token exchange with invalid_grant.
- Check connectivity to oauth2.googleapis.com and keep system time NTP-synchronized.
- Avoid mid-flow port changes — keep the default callback port free so redirect_uri matches between authorize and token calls.
- If errors persist, read the logged Google error body (invalid_client, redirect_uri_mismatch) and update happy-cli.
When it happens
Trigger: The POST to https://oauth2.googleapis.com/token fails: invalid_grant (code expired, already used, or redirect_uri differs from the authorize request), PKCE verifier mismatch, invalid_client (bad/revoked client secret), network/proxy failure, or malformed JSON response.
Common situations: Browser refresh re-sends the callback and replays a consumed code; clock skew invalidating the code within its ~10-minute lifetime; corporate proxy blocking oauth2.googleapis.com; Google-side 5xx outage; redirect_uri port drift between authorize and token calls when the default port was busy.
Related errors
- Token exchange failed
- Token exchange failed
- Token exchange failed: ${tokenResponse.statusText}
- Token exchange failed: ${error}
- Token exchange failed: ${error}
AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31).
Data as JSON: /api/errors/8fb86764d8437dc2.
Report an issue: GitHub.