{"record":{"id":"07debdf0733bdfad","repo":"siyuan-note/siyuan","slug":"oauth-flow-is-missing-or-expired","errorCode":null,"errorMessage":"OAuth flow is missing or expired","messagePattern":"OAuth flow is missing or expired","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/mcp/client/oauth.go","lineNumber":592,"sourceCode":"\t}\n\treturn base64.RawURLEncoding.EncodeToString(data), nil\n}\n\nfunc removeOAuthFlow(flowID string, flow *oauthFlow) {\n\toauthFlows.Lock()\n\tif oauthFlows.items[flowID] == flow {\n\t\tdelete(oauthFlows.items, flowID)\n\t}\n\toauthFlows.Unlock()\n}\n\nfunc CompleteMCPOAuth(flowID, code, state, callbackError, issuer string) error {\n\toauthFlows.Lock()\n\tflow := oauthFlows.items[flowID]\n\tif flow == nil || time.Now().After(flow.Expires) {\n\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}","sourceCodeStart":574,"sourceCodeEnd":610,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/mcp/client/oauth.go#L574-L610","documentation":"CompleteMCPOAuth looks up the in-memory OAuth flow registry by flowID and completes the authorization-code exchange when the browser callback arrives. This error means no flow with that ID exists or the flow's expiry time has passed; the entry is deleted and the callback is rejected. Flows are ephemeral in-memory state lost on kernel restart and expiring on a timer.","triggerScenarios":"CompleteMCPOAuth (invoked by the OAuth callback HTTP route) with a flowID that was never started, was already consumed (deleted after completion), or whose flow.Expires timestamp is in the past. Also after a kernel restart, since oauthFlows is memory-only.","commonSituations":"User takes too long in the browser to finish login and clicks authorize after expiry; user pastes an old callback URL; kernel restarted between starting the flow and the callback; duplicated callback request (e.g. browser prefetch) after the flow completed.","solutions":["Restart the OAuth authorization flow (start MCP OAuth again) to get a fresh flowID and redirect URL.","Complete the callback promptly; do not leave the login page open past the expiry window.","After a kernel restart, abandon in-flight browser flows and start over.","Ensure the callback URL is delivered exactly once; ignore duplicates after the flow completes.","Do not manually construct or reuse flowIDs; only use the ID returned when the flow was started."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := CompleteMCPOAuth(flowID, code, state, callbackErr, issuer); err != nil {\n    if strings.Contains(err.Error(), \"missing or expired\") {\n        restartOAuthFlow(server) // fresh flowID and state\n    }\n}","preventionTips":["Complete browser login promptly; flows expire on a timer","Never reuse a flowID or replay an old callback URL","Start a new flow after kernel restarts; in-memory flows are lost","Deliver the callback exactly once and ignore later duplicates"],"tags":["oauth","mcp","expiry","state-management"],"backgroundTag":"oauth-flow-expired","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}