{"record":{"id":"c953f19a62d22df3","repo":"siyuan-note/siyuan","slug":"oauth-callback-was-already-handled","errorCode":null,"errorMessage":"OAuth callback was already handled","messagePattern":"OAuth callback was already handled","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/mcp/client/oauth.go","lineNumber":608,"sourceCode":"\t\tdelete(oauthFlows.items, flowID)\n\t\toauthFlows.Unlock()\n\t\treturn fmt.Errorf(\"OAuth flow is missing or expired\")\n\t}\n\tif state != flow.State {\n\t\toauthFlows.Unlock()\n\t\treturn fmt.Errorf(\"OAuth state mismatch\")\n\t}\n\tif issuer != \"\" && issuer != flow.Issuer {\n\t\toauthFlows.Unlock()\n\t\treturn fmt.Errorf(\"OAuth issuer mismatch\")\n\t}\n\tdelete(oauthFlows.items, flowID)\n\toauthFlows.Unlock()\n\tselect {\n\tcase flow.Result <- oauthCallbackResult{Code: code, State: state, Error: callbackError}:\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"OAuth callback was already handled\")\n\t}\n}\n\nfunc IsLoopbackCallback(remoteAddr string) bool {\n\thost, _, err := net.SplitHostPort(remoteAddr)\n\tif err != nil {\n\t\treturn false\n\t}\n\tip := net.ParseIP(host)\n\treturn ip != nil && ip.IsLoopback()\n}\n\nfunc refreshOAuthCredential(ctx context.Context, client *http.Client, credential oauthCredential) (oauthCredential, bool, error) {\n\tvalues := url.Values{\n\t\t\"grant_type\":    {\"refresh_token\"},\n\t\t\"refresh_token\": {credential.RefreshToken},\n\t\t\"resource\":      {credential.Resource},\n\t}","sourceCodeStart":590,"sourceCodeEnd":626,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/mcp/client/oauth.go#L590-L626","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Treat the already-handled error as a no-op success; the first callback is authoritative.\nif err := CompleteMCPOAuth(flowID, code, state, cbErr, issuer); err != nil {\n    if strings.Contains(err.Error(), \"already handled\") {\n        return nil // first callback already completed the flow\n    }\n    return err\n}","preventionTips":["Disable the authorize button immediately after the first click.","Idempotently ignore duplicate callbacks in the handler.","Use a fresh flowID per authorization attempt."],"tags":["oauth","mcp","concurrency","duplicate-callback"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}