{"record":{"id":"8aadfa0363bffcaf","repo":"toeverything/AFFiNE","slug":"invalid-pagination-cursor","errorCode":null,"errorMessage":"Invalid pagination cursor","messagePattern":"Invalid pagination cursor","errorType":"http","errorClass":"BadRequest","httpStatus":400,"severity":"warning","filePath":"packages/backend/server/src/models/workspace-analytics.ts","lineNumber":204,"sourceCode":"}\n\nfunction parseJsonCursor<T>(cursor?: string | null): T | null {\n  if (!cursor) {\n    return null;\n  }\n\n  const raw = cursor.trim();\n  if (!raw) {\n    return null;\n  }\n\n  try {\n    return JSON.parse(raw) as T;\n  } catch {\n    try {\n      return decodeWithJson<T>(raw);\n    } catch {\n      throw new BadRequest('Invalid pagination cursor');\n    }\n  }\n}\n\nfunction parseCursorDate(value: unknown): Date {\n  if (\n    typeof value !== 'string' &&\n    typeof value !== 'number' &&\n    !(value instanceof Date)\n  ) {\n    throw new BadRequest('Invalid pagination cursor');\n  }\n\n  const parsed = new Date(value);\n  if (Number.isNaN(parsed.getTime())) {\n    throw new BadRequest('Invalid pagination cursor');\n  }\n  return parsed;","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/models/workspace-analytics.ts#L186-L222","documentation":"Thrown by parseJsonCursor (workspace-analytics.ts:204) when the `after` cursor string is neither parseable by JSON.parse nor by the project's decodeWithJson helper. All analytics list endpoints that paginate by cursor pass through this function. UserFriendlyError BadRequest, code bad_request, HTTP 400.","triggerScenarios":"Sending a malformed, truncated, or hand-edited `after` query parameter to any paginated analytics endpoint.","commonSituations":"Frontend cached an old cursor format across a server version upgrade; a proxy truncated the query string; a bookmarked URL contains a cursor that is no longer valid; client treated the cursor as editable.","solutions":["Drop the cursor and request the first page (omit the `after` parameter).","Re-encode the cursor using the server's encode helper.","Treat cursors as opaque tokens and never construct or modify them client-side."],"exampleFix":"// before\nGET /analytics?after=%%invalid\n// after\nGET /analytics   // omit `after` to start from page 1","handlingStrategy":"validation","validationCode":"function safeCursor(raw: string | null | undefined): string | null {\n  if (!raw) return null;\n  const s = raw.trim();\n  if (!s) return null;\n  try { JSON.parse(s); return s; } catch {}\n  try { decodeWithJson(s); return s; } catch {}\n  return null; // drop invalid cursor -> start from page 1\n}","typeGuard":null,"tryCatchPattern":"import { BadRequest } from '../base/error/errors.gen';\n\ntry {\n  const cursor = parseJsonCursor(pagination.after);\n} catch (e) {\n  if (e instanceof BadRequest) {\n    // drop the cursor and request the first page\n  } else throw e;\n}","preventionTips":["Treat cursors as opaque; never edit or construct them client-side.","On any cursor error, restart from page 1 rather than retrying the same value.","Invalidate cached cursors after a server version upgrade."],"tags":["pagination","cursor","analytics","input-validation"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}