siyuan-note/siyuan · warning

OAuth flow is missing or expired

Error message

OAuth flow is missing or expired

What it means

Returned at oauth.go:589-592 by CompleteMCPOAuth when the flowID lookup in oauthFlows.items returns nil OR the flow's Expires time has passed (5-minute lifetime, oauthAuthorizationTimeout). The flow either was never created, was already consumed and deleted, or expired before the callback arrived.

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 251596fc0d)

Solutions

  1. Re-initiate the OAuth authorization and complete the browser consent within the 5-minute window, then let exactly one callback through.
  2. Avoid duplicate callback tabs; the first callback consumes and deletes the flow.
  3. If the kernel was restarted, the in-memory flow is gone — start a fresh authorization; flows are intentionally not persisted.
  4. Confirm the callback flowID in the URL matches the one issued during authorization (no truncation by the AS or browser).
Defensive patterns

Strategy: validation

Try / catch

// CompleteMCPOAuth called by the callback handler — flow may already be gone.
err := CompleteMCPOAuth(flowID, code, state, callbackError, issuer)
if err != nil && strings.Contains(err.Error(), "OAuth flow is missing or expired") {
    // Render a user-facing page: 'authorization expired, please retry from SiYuan'.
    http.Error(w, "authorization flow expired; restart the authorization", http.StatusGone)
    return
}

Prevention

When it happens

Trigger: The OAuth callback HTTP request hits /api/ai/mcp/oauth/callback/<flowID> after Authorize already returned (timeout or ctx cancel which deleted the flow), or after the 5-minute Expires window, or with a flowID that was never registered (process restart wiped the in-memory map).

Common situations: User completes browser consent more than 5 minutes after starting; the original Authorize call already timed out (error 328) and removed the flow; duplicate callback (second tab) arrives after the first consumed the flow; kernel restarted between authorization start and callback.

Related errors


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