siyuan-note/siyuan · warning

OAuth callback was already handled

Error message

OAuth callback was already handled

What it means

After passing validation, CompleteMCPOAuth delivers the callback result to the flow's buffered Result channel in a non-blocking select. If no reader is waiting and the channel is full/already delivered, the callback was already handled — the waiting goroutine that consumes the result already finished (or the flow completed). This error prevents double delivery.

Solutions

  1. Ignore this error if the OAuth flow actually succeeded — it typically indicates a benign duplicate callback.
  2. Ensure the callback endpoint is called once per flow; deduplicate on the client/proxy side.
  3. Check for browser prefetch or extensions reissuing the redirect and disable them for the callback.
  4. If it appears without a successful login, restart the authorization flow.
  5. Verify no custom automation double-posts the callback.
Defensive patterns

Strategy: try-catch

Try / catch

if err := CompleteMCPOAuth(flowID, code, state, callbackErr, issuer); err != nil {
    if strings.Contains(err.Error(), "already handled") {
        log.Info("duplicate OAuth callback ignored") // benign; flow already finished
    }
}

Prevention

When it happens

Trigger: CompleteMCPOAuth invoked a second time for the same logical callback after the Result channel was already consumed: duplicate HTTP callback requests (browser retry/prefetch), the user reloading the callback URL, or two callback routes firing for one flow.

Common situations: Browser resends the redirect request; user refreshes the callback page; a proxy retries the request; monitoring/crawler hits the callback URL with the same parameters after completion.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/c953f19a62d22df3. Report an issue: GitHub.

Appendix: 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 9f775e8a12)