{"record":{"id":"969efccea3bf43f3","repo":"toeverything/AFFiNE","slug":"too-many-request","errorCode":"too_many_request","errorMessage":"Too many requests.","messagePattern":"Too many requests\\.","errorType":"exception","errorClass":"TooManyRequest","httpStatus":429,"severity":"warning","filePath":"packages/backend/server/src/core/auth/session-exchange.ts","lineNumber":116,"sourceCode":"      userSessionId: userSession.id,\n      ...metadata,\n    });\n    return this.tokenPair(\n      payload.userId,\n      issued.session.id,\n      issued.refreshToken,\n      issued.refreshExpiresAt,\n      issued.session.absoluteExpiresAt\n    );\n  }\n\n  async refresh(req: Request, refreshToken: string, appVersion?: string) {\n    if (!isNativeClientRequest(req)) throw new ActionForbidden();\n    const selector = refreshToken.split('.')[1];\n    if (selector) {\n      const rateKey = `auth:session-refresh-rate:${selector}`;\n      const attempts = await this.cache.increaseWithTtl(rateKey, 60_000);\n      if (attempts > 30) throw new TooManyRequest();\n    }\n    const refreshed = await this.authSessions.refresh(refreshToken, appVersion);\n    if (refreshed.status !== 'rotated') {\n      const status =\n        refreshed.code === AuthSessionErrorCode.temporarilyUnavailable\n          ? HttpStatus.SERVICE_UNAVAILABLE\n          : HttpStatus.UNAUTHORIZED;\n      throw new AuthSessionHttpError(refreshed.code, status);\n    }\n    const session = await this.authSessions.get(refreshed.authSessionId);\n    if (!session) {\n      throw new AuthSessionHttpError(AuthSessionErrorCode.revoked);\n    }\n    return this.tokenPair(\n      session.userSession.userId,\n      refreshed.authSessionId,\n      refreshed.refreshToken,\n      refreshed.refreshExpiresAt,","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/session-exchange.ts#L98-L134","documentation":"Thrown by SessionExchangeService.refresh when more than 30 refresh attempts are made within a 60-second sliding window keyed by the refresh token's selector (the segment after the first '.'). Category 'too_many_requests', code 'too_many_request'. The counter is incremented via cache.increaseWithTtl and is per-selector, so it targets a single (possibly compromised) token, not a whole IP.","triggerScenarios":"Calling refresh (session-exchange.ts:112-116) more than 30 times in 60s with a refresh token whose selector portion is identical. Each call increments the counter regardless of success.","commonSituations":"A bug in the client causing a refresh loop (e.g. retrying on every 401, including the 401 from this very rate limit); a malicious actor hammering a leaked refresh token; multiple app instances/tabs all refreshing at once with the same token; an aggressive background poller.","solutions":["Stop the refresh retry loop: only refresh once per access-token expiry, and treat a 429 by backing off (not by retrying immediately).","Ensure each device/tab holds its own session and refresh token rather than sharing one.","If the token leaked, revoke the session and force a fresh sign-in."],"exampleFix":"// before: retry on any non-200, including 429\nwhile (!(await refresh()).ok) { /* loops forever */ }\n\n// after: respect 429 with backoff and no immediate retry\nconst res = await refresh();\nif (res.status === 429) {\n  const retryAfter = Number(res.headers.get('retry-after') ?? 60);\n  await sleep(retryAfter * 1000);\n}","handlingStrategy":"retry","validationCode":"const REFRESH_WINDOW_MS = 60_000;\nconst REFRESH_MAX = 30;\nfunction canRefreshNow(lastAttempts: number[]): boolean {\n  const now = Date.now();\n  const recent = lastAttempts.filter(t => now - t < REFRESH_WINDOW_MS);\n  return recent.length < REFRESH_MAX;\n}","typeGuard":"function hasSelector(refreshToken: string): boolean {\n  const seg = refreshToken.split('.')[1];\n  return typeof seg === 'string' && seg.length > 0;\n}","tryCatchPattern":"try {\n  await refresh(req, refreshToken, appVersion);\n} catch (e) {\n  if (e.code === 'too_many_request') {\n    const retryAfter = 60; // selector window is 60s\n    await sleep(retryAfter * 1000);\n    // then retry at most once; if it 429s again, sign the user in fresh\n  } else throw e;\n}","preventionTips":["Refresh at most once per access-token expiry, not on a fixed short interval.","Treat a 429 as a signal to back off, never to retry immediately in a loop.","Give each device/tab its own session rather than sharing one refresh token."],"tags":["auth","rate-limit","refresh","session","throttling"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}