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
- Close all stale authorization tabs from earlier attempts, then re-run login once
- Ensure only one login flow is active at a time (one process, one port)
- Never re-paste or bookmark the localhost callback URL — it is single-use and state-bound
- 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
- Close authorization tabs from previous attempts before re-logging in
- Run exactly one login flow per port at a time
- Never replay or bookmark the localhost callback URL
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
- starting callback server on port %d: %w
- State mismatch
- manual input canceled
- could not find authorization code in input
- authentication timed out after 5 minutes
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/60fa8068b82e4d4d.
Report an issue: GitHub.