sipeed/picoclaw · error

state mismatch

Error message

state mismatch

What it means

The OAuth callback arrived with a state query parameter that did not match the state generated at flow start (oauth.go:195). This CSRF guard exists to bind the callback to this login attempt. Mismatches come from stale browser tabs replaying an old callback, two concurrent logins sharing the port, or (rarely) forged requests probing the loopback listener.

Source

Thrown at pkg/auth/oauth.go:195

		}
		if code == "" {
			return nil, fmt.Errorf("could not find authorization code in input")
		}
		return ExchangeCodeForTokens(cfg, code, pkce.CodeVerifier, redirectURI)
	case <-time.After(5 * time.Minute):
		return nil, fmt.Errorf("authentication timed out after 5 minutes")
	}
}

func oauthCallbackRedirectURI(port int) string {
	return fmt.Sprintf("http://localhost:%d/auth/callback", port)
}

func oauthCallbackHandler(state string, resultCh chan<- callbackResult) http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("/auth/callback", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Query().Get("state") != state {
			resultCh <- callbackResult{err: fmt.Errorf("state mismatch")}
			http.Error(w, "State mismatch", http.StatusBadRequest)
			return
		}

		code := r.URL.Query().Get("code")
		if code == "" {
			errMsg := r.URL.Query().Get("error")
			resultCh <- callbackResult{err: fmt.Errorf("no code received: %s", errMsg)}
			http.Error(w, "No authorization code received", http.StatusBadRequest)
			return
		}

		w.Header().Set("Content-Type", "text/html")
		fmt.Fprint(w, "<html><body><h2>Authentication successful!</h2><p>You can close this window.</p></body></html>")
		resultCh <- callbackResult{code: code}
	})
	return mux
}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Close all stale authorization tabs from earlier attempts, then re-run login once
  2. Ensure only one login flow is active at a time (one process, one port)
  3. Never re-paste or bookmark the localhost callback URL — it is single-use and state-bound
  4. On mismatch, always restart the flow; the code exchange cannot proceed with mismatched state
Defensive patterns

Strategy: try-catch

Try / catch

if err != nil && strings.Contains(err.Error(), "state mismatch") {
    // stale tab or concurrent login: never exchange, always restart
    fmt.Println("stale OAuth callback (state mismatch) — close old auth tabs and re-run login")
    return err
}

Prevention

When it happens

Trigger: An old auth page left open from a previous run redirects after a new LoginBrowser started on the same port; running two logins concurrently so each server receives the other's callback; scripted/manual replay of a captured callback URL.

Common situations: User retries login while the previous browser tab is still mid-flow; browser restoring pinned tabs that re-fire old redirects; automated tests hitting the callback handler with a canned state.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/60fa8068b82e4d4d. Report an issue: GitHub.