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

  1. Treat the first successful callback as authoritative; ignore subsequent ones (this error is informational, not data-loss).
  2. If the first callback never produced a token, restart the OAuth flow to get a new `flowID` and `state`.
  3. 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

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


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/c953f19a62d22df3. Report an issue: GitHub.