siyuan-note/siyuan · error

OAuth flow is missing or expired

Error message

OAuth flow is missing or expired

What it means

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.

Solutions

  1. Restart the OAuth authorization flow (start MCP OAuth again) to get a fresh flowID and redirect URL.
  2. Complete the callback promptly; do not leave the login page open past the expiry window.
  3. After a kernel restart, abandon in-flight browser flows and start over.
  4. Ensure the callback URL is delivered exactly once; ignore duplicates after the flow completes.
  5. Do not manually construct or reuse flowIDs; only use the ID returned when the flow was started.
Defensive patterns

Strategy: try-catch

Try / catch

if err := CompleteMCPOAuth(flowID, code, state, callbackErr, issuer); err != nil {
    if strings.Contains(err.Error(), "missing or expired") {
        restartOAuthFlow(server) // fresh flowID and state
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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

Appendix: source

Thrown at kernel/mcp/client/oauth.go:592

	}
	return base64.RawURLEncoding.EncodeToString(data), nil
}

func removeOAuthFlow(flowID string, flow *oauthFlow) {
	oauthFlows.Lock()
	if oauthFlows.items[flowID] == flow {
		delete(oauthFlows.items, flowID)
	}
	oauthFlows.Unlock()
}

func CompleteMCPOAuth(flowID, code, state, callbackError, issuer string) error {
	oauthFlows.Lock()
	flow := oauthFlows.items[flowID]
	if flow == nil || time.Now().After(flow.Expires) {
		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")
	}
}

View on GitHub (pinned to 9f775e8a12)