lbjlaq/Antigravity-Manager · error
OAuth state mismatch
Error message
OAuth state mismatch
What it means
This error is produced by the local loopback OAuth callback server started in ensure_oauth_flow_prepared (src-tauri/src/modules/oauth_server.rs). When the browser redirects back to the IPv4 listener with an authorization code, the handler compares the callback's `state` query parameter against the state stored in OAUTH_FLOW_STATE; if a code is present but the state does not match (or no flow state exists), the CSRF check fails and 'OAuth state mismatch' is sent back through the code channel and returned to the caller of prepare_oauth_url/start_oauth_flow/complete_oauth_flow. It is a deliberate CSRF-protection rejection, not a network fault.
Solutions
- Start a fresh OAuth flow (prepare_oauth_url/start_oauth_flow again) and open the NEW auth URL — do not reuse a previously opened authorization tab.
- Only click 'Start OAuth' once per attempt and complete the login in the tab opened from that same auth URL.
- Check the app log for 'OAuth callback state mismatch (CSRF protection)' and confirm whether another flow was started concurrently; serialize OAuth flows in the UI so only one is active.
- If switching accounts/clients, cancel and restart the flow rather than mixing auth URLs across client keys.
- Verify system clock/browser session sanity; if the mismatch is reproducible with a single fresh flow, report it since state is generated in-memory with uuid::Uuid::new_v4.
Example fix
// before: reusing a stale auth URL tab after restarting the flow open_browser(old_auth_url); // state already regenerated -> mismatch // after: always fetch a fresh URL from the current flow let url = start_oauth_flow(client_key).await?; open_browser(url);
Defensive patterns
Strategy: try-catch
Validate before calling
// Before completing the flow, confirm a fresh auth URL was obtained in THIS attempt:
const url = await invoke('prepare_oauth_url', { clientKey });
if (!url.includes('state=')) throw new Error('auth URL missing state param');
// Always open this exact URL; never reuse a previously opened tab. Type guard
function isStateMismatchError(e: unknown): e is string {
return typeof e === 'string' && e === 'OAuth state mismatch';
} Try / catch
try {
const code = await invoke('complete_oauth_flow', { clientKey });
} catch (e) {
if (isStateMismatchError(e)) {
// stale flow: restart with a brand-new auth URL
const freshUrl = await invoke('start_oauth_flow', { clientKey });
openBrowser(freshUrl);
} else throw e;
} Prevention
- Always open the auth URL returned by the current flow invocation; discard old tabs/URLs.
- Never start a second OAuth flow while one is pending; cancel explicitly first.
- Don't change the client key mid-flow; restarting invalidates the old state by design.
- Complete login promptly; long delays increase the chance the flow was reset in between.
- Log which listener (IPv4/IPv6) reported the mismatch to distinguish stale-tab vs concurrency causes.
When it happens
Trigger: A callback request hits the IPv4 listener (127.0.0.1:<port>/oauth-callback) carrying a `code` parameter but a `state` value that differs from the stored OAuthFlowState.state — e.g. the flow was re-prepared/cancelled (state reset to None) between when the auth URL was opened and when the browser redirected back, the user reused an old auth-URL tab, or two OAuth flows were started concurrently.
Common situations: User opens the authorization page, restarts the OAuth flow in the app (which calls ensure_oauth_flow_prepared again and regenerates state), then completes login in the stale tab; a second client key was requested, cancelling the in-flight flow (s.client_key != requested_key drops the old state); the browser cached/replayed an old callback URL; a slow authorization finished after the flow was force-cancelled because code_rx had already been taken.
Related errors
AI-assisted analysis of lbjlaq/Antigravity-Manager@90a2b6b207 (2026-09-19).
Data as JSON: /api/errors/2a55d8290430dc29.
Report an issue: GitHub.
Appendix: source
Thrown at src-tauri/src/modules/oauth_server.rs:231
received_state.as_ref() == Some(&s.state)
} else {
false
}
} else {
false
}
};
let (result, response_html) = match (code, state_valid) {
(Some(code), true) => {
crate::modules::logger::log_info(
"Successfully captured OAuth code from IPv4 listener",
);
(Ok(code), oauth_success_html())
}
(Some(_), false) => {
crate::modules::logger::log_error(
"OAuth callback state mismatch (CSRF protection)",
);
(Err("OAuth state mismatch".to_string()), oauth_fail_html())
}
(None, _) => (
Err("Failed to get Authorization Code in callback".to_string()),
oauth_fail_html(),
),
};
let _ = stream.write_all(response_html.as_bytes()).await;
let _ = stream.flush().await;
if let Some(h) = app_handle {
use tauri::Emitter;
let _ = h.emit("oauth-callback-received", ());
}
let _ = tx.send(result).await;
}View on GitHub (pinned to 90a2b6b207)