{"record":{"id":"e3807a6af8119411","repo":"zeroclaw-labs/zeroclaw","slug":"oauth-state-mismatch","errorCode":null,"errorMessage":"OAuth state mismatch","messagePattern":"OAuth state mismatch","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-providers/src/auth/gemini_oauth.rs","lineNumber":372,"sourceCode":"        accept_result = async {\n            tokio::time::timeout(timeout, listener.accept()).await\n        } => {\n            match accept_result {\n                Ok(Ok((mut stream, _))) => {\n                    let mut buffer = vec![0u8; 4096];\n                    let n = stream\n                        .read(&mut buffer)\n                        .await\n                        .context(\"Failed to read from callback connection\")?;\n\n                    let request = String::from_utf8_lossy(&buffer[..n]);\n                    let (code, state) = parse_callback_request(&request)?;\n\n                    if state != expected_state {\n                        let response = \"HTTP/1.1 400 Bad Request\\r\\nContent-Type: text/html\\r\\n\\r\\n\\\n                             <html><body><h1>State mismatch</h1><p>Please try again.</p></body></html>\";\n                        let _ = stream.write_all(response.as_bytes()).await;\n                        anyhow::bail!(\"OAuth state mismatch\");\n                    }\n\n                    let response = \"HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\n\\r\\n\\\n                         <html><body><h1>Success!</h1><p>You can close this window and return to the terminal.</p></body></html>\";\n                    let _ = stream.write_all(response.as_bytes()).await;\n\n                    Ok(code)\n                }\n                Ok(Err(e)) => {\n                    ::zeroclaw_log::record!(\n                        ERROR,\n                        ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)\n                            .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                            .with_attrs(::serde_json::json!({\n                                \"oauth_provider\": \"gemini\",\n                                \"phase\": \"callback_accept\",\n                                \"error\": format!(\"{}\", e),\n                            })),","sourceCodeStart":354,"sourceCodeEnd":390,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-providers/src/auth/gemini_oauth.rs#L354-L390","documentation":"The loopback callback server (127.0.0.1:1456) accepted a browser request to /auth/callback whose state query parameter differs from the PKCE state generated for the current pending login. This is the CSRF protection of the OAuth flow: a matching state proves the callback belongs to this login attempt. The server answers the browser with HTTP 400 and a 'State mismatch' page before bailing.","triggerScenarios":"receive_loopback_code is waiting for the redirect and receives a callback whose state is from a different, earlier authorize URL — e.g. the user finally loads a stale browser tab bookmarked from a previous auth login run, or two zeroclaw login processes share port 1456 and one catches the other's redirect.","commonSituations":"Re-running auth login while an old consent tab is still open; two terminals running gemini/openai logins concurrently on the same machine (both bind localhost:1456); browser extension or privacy tool reissuing the navigation with a mangled query string.","solutions":["Close all leftover Google consent tabs from earlier attempts and re-run auth login from scratch","Run only one OAuth login at a time on the machine so port 1456 is owned by a single flow","If the browser flow keeps failing, use the printed manual path: copy the final callback URL and paste it at the stdin prompt, or use auth paste-redirect"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Ensure only one flow owns the callback port before listening.\nmatch tokio::net::TcpListener::bind(\"127.0.0.1:1456\").await {\n    Ok(l) => { /* safe to run the loopback flow */ let _ = l; }\n    Err(_) => { /* another flow owns 1456; use paste-redirect instead */ }\n}","typeGuard":null,"tryCatchPattern":"match receive_loopback_code(&pkce.state, timeout).await {\n    Ok(code) => code,\n    Err(e) if e.to_string().contains(\"state mismatch\") => {\n        // stale tab or concurrent login hit the callback; restart cleanly\n        eprintln!(\"close old consent tabs, then retry\");\n        receive_loopback_code(&pkce.state, timeout).await?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Close consent tabs from earlier login attempts before re-running auth login","Run one OAuth login at a time per machine (port 1456 is shared)","Fall back to auth paste-redirect when the loopback path keeps mismatching"],"tags":["oauth","csrf","state-mismatch","loopback-callback","gemini","rust"],"backgroundTag":"oauth-state-mismatch","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}