{"record":{"id":"641fd9997b7d97ca","repo":"toeverything/AFFiNE","slug":"oauth-state-expired","errorCode":"oauth_state_expired","errorMessage":"OAuth state expired, please try again.","messagePattern":"OAuth state expired, please try again\\.","errorType":"exception","errorClass":"OauthStateExpired","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/plugins/calendar/controller.ts","lineNumber":127,"sourceCode":"    res: Response,\n    code?: string,\n    stateStr?: string\n  ) {\n    if (!code) {\n      throw new MissingOauthQueryParameter({ name: 'code' });\n    }\n\n    if (!stateStr) {\n      throw new MissingOauthQueryParameter({ name: 'state' });\n    }\n\n    if (typeof stateStr !== 'string' || !this.oauth.isValidState(stateStr)) {\n      throw new MissingOauthQueryParameter({ name: 'state' });\n    }\n\n    const state = await this.oauth.getOAuthState(stateStr);\n    if (!state) {\n      throw new OauthStateExpired();\n    }\n\n    const callbackUrl = this.calendar.getCallbackUrl();\n    try {\n      await this.calendar.handleOAuthCallback({\n        provider: state.provider,\n        code,\n        redirectUri: callbackUrl,\n        userId: state.userId,\n      });\n    } catch (error) {\n      if (state.redirectUri) {\n        const message = this.getCallbackErrorMessage(error);\n        const redirectUrl = this.buildErrorRedirect(state.redirectUri, message);\n        return this.url.safeRedirect(res, redirectUrl);\n      }\n      throw error;\n    }","sourceCodeStart":109,"sourceCodeEnd":145,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/calendar/controller.ts#L109-L145","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await handleCallback(code, state);\n} catch (e) {\n  if (e instanceof OauthStateExpired) {\n    // bounded single retry: re-run preflight to mint a fresh state,\n    // re-redirect the user to the new authorization URL; never loop\n  } else throw e;\n}","preventionTips":["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."],"tags":["calendar","oauth","state","expiry","redis"],"backgroundTag":"oauth-state-expired","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-08-23T06:17:17.905Z"}