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

  1. Restart the connect flow to generate a fresh state and use only the newest authorization URL
  2. Ensure only one authentication attempt runs at a time; complete or cancel the previous one
  3. Verify no proxy or browser extension is stripping the state query parameter from the redirect URL
  4. 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

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


AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31). Data as JSON: /api/errors/6448bbdd46bb1536. Report an issue: GitHub.