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

  1. Restart the flow: call preflight again, redirect to the new authorization URL, and complete promptly.
  2. Review the OAuth state TTL configuration against real-world usage patterns.
  3. 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

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


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/641fd9997b7d97ca. Report an issue: GitHub.