sipeed/picoclaw · error
State mismatch
Error message
State mismatch
What it means
During OAuth login the library starts a localhost callback server (127.0.0.1) and compares the state query parameter of the /auth/callback request against the randomly generated state issued in the authorize URL. A mismatch fails this check: the callback does not correspond to the current authorization request, which is the standard CSRF/replay protection for the authorization code flow.
Source
Thrown at pkg/auth/oauth.go:196
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
- Start a fresh authorize URL from the app for every attempt — never reuse or bookmark login links
- Ensure only one auth flow runs at a time per port/profile
- Retry the login end-to-end (new state is generated automatically)
- If it persists, check that no other local process is competing for the callback port
Defensive patterns
Strategy: retry
Try / catch
token, err := auth.Login(ctx)
if err != nil {
if strings.Contains(err.Error(), "state mismatch") {
// stale callback (old tab/bookmark) or concurrent flow — a fresh attempt gets a new state
token, err = auth.Login(ctx)
}
if err != nil {
return err
}
} Prevention
- Always start login from a freshly issued authorize URL; never reuse bookmarked login links
- Serialize logins — one in-flight flow per profile/port at a time
- On restart, discard browser state from the previous session instead of resuming its consent page
When it happens
Trigger: (1) Reusing an old authorize URL or reloaded callback page after the flow restarted (state rotates per attempt); (2) two concurrent logins — the other flow's callback (different state) hits this listener; (3) port reuse: a previous flow's listener closed and the port was handed to a new flow while the browser still had the old tab; (4) an unrelated/crafted request probing the callback URL.
Common situations: User refreshes the callback page or reuses a bookmarked login link; multiple tabs/tokens initiating auth at once; CLI restarted mid-flow while the browser sat on the consent page; security scanners hitting localhost callback endpoints.
Related errors
- state mismatch
- insufficient scope: usage endpoint requires oauth scope
- No authorization code received
- Invalid browser login response
- Invalid device code response
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/fefea06efc12db98.
Report an issue: GitHub.