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

  1. 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.
  2. Only click 'Start OAuth' once per attempt and complete the login in the tab opened from that same auth URL.
  3. 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.
  4. If switching accounts/clients, cancel and restart the flow rather than mixing auth URLs across client keys.
  5. 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

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)