siyuan-note/siyuan · warning
OAuth callback was already handled
Error message
OAuth callback was already handled
What it means
The OAuth callback's result has already been delivered: the `flow.Result` channel has no waiting receiver, so the `select` statement falls through to its `default` branch. The flow record was already deleted from `oauthFlows`, so a second/duplicate callback cannot enqueue its result.
Source
Thrown at kernel/mcp/client/oauth.go:608
delete(oauthFlows.items, flowID)
oauthFlows.Unlock()
return fmt.Errorf("OAuth flow is missing or expired")
}
if state != flow.State {
oauthFlows.Unlock()
return fmt.Errorf("OAuth state mismatch")
}
if issuer != "" && issuer != flow.Issuer {
oauthFlows.Unlock()
return fmt.Errorf("OAuth issuer mismatch")
}
delete(oauthFlows.items, flowID)
oauthFlows.Unlock()
select {
case flow.Result <- oauthCallbackResult{Code: code, State: state, Error: callbackError}:
return nil
default:
return fmt.Errorf("OAuth callback was already handled")
}
}
func IsLoopbackCallback(remoteAddr string) bool {
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
return false
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}
func refreshOAuthCredential(ctx context.Context, client *http.Client, credential oauthCredential) (oauthCredential, bool, error) {
values := url.Values{
"grant_type": {"refresh_token"},
"refresh_token": {credential.RefreshToken},
"resource": {credential.Resource},
}View on GitHub (pinned to 251596fc0d)
Solutions
- Treat the first successful callback as authoritative; ignore subsequent ones (this error is informational, not data-loss).
- If the first callback never produced a token, restart the OAuth flow to get a new `flowID` and `state`.
- On the client UI, disable the authorize action immediately after the first click to prevent duplicate redirects.
Defensive patterns
Strategy: try-catch
Try / catch
// Treat the already-handled error as a no-op success; the first callback is authoritative.
if err := CompleteMCPOAuth(flowID, code, state, cbErr, issuer); err != nil {
if strings.Contains(err.Error(), "already handled") {
return nil // first callback already completed the flow
}
return err
} Prevention
- Disable the authorize button immediately after the first click.
- Idempotently ignore duplicate callbacks in the handler.
- Use a fresh flowID per authorization attempt.
When it happens
Trigger: A second callback arrives for a `flowID` whose first callback already sent on `flow.Result`, or the goroutine waiting on `Result` has exited (timeout/cancellation) before the callback is delivered.
Common situations: Browser retries the redirect (refresh, back button), the IdP delivers both a front-channel and back-channel callback, or the user clicked the authorize button twice rapidly.
Related errors
- mcp oauth authorization required
- parse OAuth challenge: %w
- server returned %s without an OAuth Bearer challenge
- server returned %s
- discover OAuth authorization server: %w
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/c953f19a62d22df3.
Report an issue: GitHub.