slopus/happy · error
State mismatch. Possible CSRF attack
Error message
State mismatch. Possible CSRF attack
What it means
In the Gemini/Google OAuth callback server, the received `state` query parameter is compared with the state generated when authenticateGemini() built the authorization URL. A mismatch is treated as a possible CSRF attack: the browser gets 'State mismatch. Possible CSRF attack' (HTTP 400) and the promise is rejected with 'Invalid state parameter'. This protects the local redirect endpoint from forged or replayed callbacks.
Source
Thrown at packages/happy-cli/src/commands/connect/authenticateGemini.ts:144
const url = new URL(req.url!, `http://localhost:${port}`);
if (url.pathname === '/oauth2callback') {
const code = url.searchParams.get('code');
const receivedState = url.searchParams.get('state');
const error = url.searchParams.get('error');
if (error) {
res.writeHead(302, {
'Location': 'https://developers.google.com/gemini-code-assist/auth_failure_gemini'
});
res.end();
server.close();
reject(new Error(`Authentication error: ${error}`));
return;
}
if (receivedState !== state) {
res.writeHead(400);
res.end('State mismatch. Possible CSRF attack');
server.close();
reject(new Error('Invalid state parameter'));
return;
}
if (!code) {
res.writeHead(400);
res.end('No authorization code received');
server.close();
reject(new Error('No authorization code received'));
return;
}
try {
// Exchange code for tokens
const tokens = await exchangeCodeForTokens(code, verifier, port);
View on GitHub (pinned to b824cd0a46)
Solutions
- Close all old Google auth tabs/windows, then rerun `happy` connect so the state in the URL matches the running callback server.
- Make sure only one connect flow is running at a time (don't run connect in two terminals concurrently).
- Open the exact auth URL the CLI just printed — do not reuse an earlier URL whose state has expired.
- If a rogue process is squatting on port 54545, kill it or let the CLI pick a fresh port, then retry.
Example fix
// before: completing an old consent tab // old tab: http://localhost:54545/oauth2callback?code=...&state=<old-state> → rejected // after: close old tabs, rerun code: await authenticateGemini(); // fresh state matches fresh callback
Defensive patterns
Strategy: try-catch
Validate before calling
// Ensure the callback port is free and no other auth flow is running
const portInUse = !(await isPortFree(54545));
if (portInUse) console.warn('Port 54545 busy — another Gemini auth flow may be running; close it before reconnecting'); Try / catch
try {
const tokens = await authenticateGemini();
} catch (err) {
if (err instanceof Error && /Invalid state parameter|CSRF/i.test(err.message)) {
console.error('State mismatch — an old or forged callback reached the server. Close old Google auth tabs and rerun connect.');
} else throw err;
} Prevention
- Close previous Google consent tabs before starting a new connect flow.
- Never run two Gemini authentications concurrently; each generates its own state and they can cross callbacks.
- Always use the auth URL printed by the current run, not a saved one.
- If the default port is occupied by another happy process, stop that process so states don't get mixed across servers.
When it happens
Trigger: http://localhost:<port>/oauth2callback is hit with a `state` that differs from this run's generated state — stale tab from a prior auth attempt completing the callback, two concurrent authenticateGemini() runs racing on the same port, or the state param dropped/rewritten in transit.
Common situations: User re-runs Gemini connect while an old Google consent tab is still open and completes the stale one; Google session already authorized so Google redirects immediately with an old cached flow; a bookmarked callback URL from a previous session; port 54545 occupied by another happy process handling its own state.
Related errors
- Invalid state parameter
- Invalid state parameter
- No authorization code received
- Token exchange failed: ${tokenResponse.statusText}
- Token exchange failed: ${error}
AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31).
Data as JSON: /api/errors/8a36d9f56b560af9.
Report an issue: GitHub.