slopus/happy · error
Invalid state parameter
Error message
Invalid state parameter
What it means
During OAuth callback handling, startCallbackServer validates that the state query parameter returned by the authorization server matches the state originally generated. A mismatch causes a 400 response and rejects the authentication promise with 'Invalid state parameter' to prevent CSRF.
Source
Thrown at packages/happy-cli/src/commands/connect/authenticateClaude.ts:147
/**
* Start local server to handle OAuth callback
*/
async function startCallbackServer(
state: string,
verifier: string,
port: number
): Promise<ClaudeAuthTokens> {
return new Promise((resolve, reject) => {
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
const url = new URL(req.url!, `http://localhost:${port}`);
if (url.pathname === '/callback') {
const code = url.searchParams.get('code');
const receivedState = url.searchParams.get('state');
if (receivedState !== state) {
res.writeHead(400);
res.end('Invalid state parameter');
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, state);
View on GitHub (pinned to b824cd0a46)
Solutions
- Restart the connect flow to generate a fresh state and use only the newest authorization URL
- Ensure only one authentication attempt runs at a time; complete or cancel the previous one
- Verify no proxy or browser extension is stripping the state query parameter from the redirect URL
- Retry — transient provider-side issues can mangle redirect parameters
Example fix
// before
const receivedState = url.searchParams.get('state');
if (receivedState !== state) { ... }
// after
const receivedState = url.searchParams.get('state');
if (!receivedState || !timingSafeEqual(Buffer.from(receivedState), Buffer.from(state))) { ... } Defensive patterns
Strategy: retry
Validate before calling
// before opening the browser, ensure a single fresh flow:
const expectedState = generateState(); // must be the state passed into startCallbackServer
// compare against the URL you open:
console.assert(authorizeUrl.includes(`state=${expectedState}`), 'state missing from authorize URL'); Type guard
function hasValidState(url: URL, expected: string): boolean {
const s = url.searchParams.get('state');
return typeof s === 'string' && s.length > 0 && s === expected;
} Try / catch
try {
const tokens = await authenticateClaude();
} catch (e) {
if (e.message === 'Invalid state parameter') {
console.error('Stale or duplicate OAuth callback; restart the connect flow and use the newest URL');
} else throw e;
} Prevention
- Run only one auth flow at a time; cancel stale ones
- Always open the freshly generated authorization URL, never a bookmarked one
- Don't re-open or refresh the callback URL after it has been consumed
- Verify proxies/extensions don't strip query parameters
When it happens
Trigger: The OAuth provider redirects to /callback with a state value differing from the generated one: stale callback URL reuse, multiple concurrent auth flows clobbering state, provider stripping/altering the query string, or a forged callback.
Common situations: Reusing an old authorize URL after restarting the flow; running two authentication attempts in parallel in different terminals; proxy/redirect middleware dropping query parameters; expired session state.
Related errors
- Invalid state parameter
- State mismatch. Possible CSRF attack
- 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/6448bbdd46bb1536.
Report an issue: GitHub.