toeverything/AFFiNE · error · OauthStateExpired
oauth_state_expired
oauth_state_expired
Error message
OAuth state expired, please try again.
What it means
The state token passes format validation but getOAuthState(stateStr) returns null: the stored state has expired (server-side TTL) or was already consumed. The error tells the user to retry — a new preflight generates a fresh state and authorization URL.
Source
Thrown at packages/backend/server/src/plugins/calendar/controller.ts:127
res: Response,
code?: string,
stateStr?: string
) {
if (!code) {
throw new MissingOauthQueryParameter({ name: 'code' });
}
if (!stateStr) {
throw new MissingOauthQueryParameter({ name: 'state' });
}
if (typeof stateStr !== 'string' || !this.oauth.isValidState(stateStr)) {
throw new MissingOauthQueryParameter({ name: 'state' });
}
const state = await this.oauth.getOAuthState(stateStr);
if (!state) {
throw new OauthStateExpired();
}
const callbackUrl = this.calendar.getCallbackUrl();
try {
await this.calendar.handleOAuthCallback({
provider: state.provider,
code,
redirectUri: callbackUrl,
userId: state.userId,
});
} catch (error) {
if (state.redirectUri) {
const message = this.getCallbackErrorMessage(error);
const redirectUrl = this.buildErrorRedirect(state.redirectUri, message);
return this.url.safeRedirect(res, redirectUrl);
}
throw error;
}View on GitHub (pinned to b4c8548c09)
Solutions
- Restart the flow: call preflight again, redirect to the new authorization URL, and complete promptly.
- Review the OAuth state TTL configuration against real-world usage patterns.
- If Redis was flushed or restarted without persistence, expect all stored states to be lost — users must restart their flows.
Defensive patterns
Strategy: retry
Try / catch
try {
await handleCallback(code, state);
} catch (e) {
if (e instanceof OauthStateExpired) {
// bounded single retry: re-run preflight to mint a fresh state,
// re-redirect the user to the new authorization URL; never loop
} else throw e;
} Prevention
- Complete the OAuth consent promptly after preflight; do not bookmark callback URLs.
- Size the state TTL generously versus real consent-screen dwell time.
- Persist Redis state keys across restarts if users may have flows in flight.
When it happens
Trigger: Completing the calendar OAuth consent long after preflight was run (TTL elapsed), reloading or bookmarking an old callback URL, or completing the flow twice with the same state.
Common situations: User leaves the consent screen open past the state TTL; shared/bookmarked callback links; Redis flush or restart dropping the state keys; overly short state TTL configuration.
Related errors
- missing_oauth_query_parameter
- unknown_oauth_provider
- invalid_oauth_response
- missing_oauth_query_parameter
- calendar_provider_oauth_unsupported
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/641fd9997b7d97ca.
Report an issue: GitHub.