{"record":{"id":"652fdbef6934dc43","repo":"immich-app/immich","slug":"invalid-cursor","errorCode":null,"errorMessage":"Invalid cursor","messagePattern":"Invalid cursor","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/utils/search-cursor.ts","lineNumber":19,"sourceCode":"import { BadRequestException } from '@nestjs/common';\nimport z from 'zod';\n\nconst SearchCursorPayloadSchema = z.object({\n  offset: z.int().min(0),\n});\n\nexport const encodeSearchCursor = (offset: number): string =>\n  Buffer.from(JSON.stringify({ offset } satisfies z.infer<typeof SearchCursorPayloadSchema>)).toString('base64url');\n\nexport const decodeSearchCursor = (cursor?: string): { offset: number } => {\n  if (cursor === undefined) {\n    return { offset: 0 };\n  }\n\n  try {\n    return SearchCursorPayloadSchema.parse(JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8')));\n  } catch {\n    throw new BadRequestException('Invalid cursor');\n  }\n};\n","sourceCodeStart":1,"sourceCodeEnd":22,"githubUrl":"https://github.com/immich-app/immich/blob/e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c/server/src/utils/search-cursor.ts#L1-L22","documentation":"decodeSearchCursor() decodes an opaque base64url cursor from a paginated search request and validates it against SearchCursorPayloadSchema with Zod. Any failure to base64-decode, JSON-parse, or schema-validate the cursor throws a BadRequestException('Invalid cursor'), because a corrupted or fabricated cursor cannot be trusted to resume pagination.","triggerScenarios":"Calling a paginated search endpoint (metadata search / smart search) with a cursor query parameter that is not valid base64url, not valid JSON, or whose payload fails schema validation (missing/mistyped fields like offset).","commonSituations":"Hand-truncating or hand-editing a cursor string; mixing cursors between API versions after a server upgrade changed the payload schema; urlencoding stripping characters from the cursor.","solutions":["Request the first page by omitting the cursor parameter (server returns { offset: 0 } equivalent) and use a fresh cursor from the previous response.","Ensure the cursor is transmitted verbatim (URL-encoded) — do not trim, decode, or re-encode it.","If cursors persist across a server upgrade, discard stored cursors and restart pagination.","Client-side: wrap cursor usage so any 400 'Invalid cursor' restarts pagination from page one."],"exampleFix":"// before\nconst page = await api.search({ cursor: cursor.slice(0, 20) });\n// after\nconst page = await api.search({ cursor }); // pass cursor unmodified, or omit for page 1","handlingStrategy":"try-catch","validationCode":"function isPlausibleCursor(c) {\n  try { return !!JSON.parse(Buffer.from(c, 'base64url').toString('utf8')).hasOwnProperty('offset'); } catch { return false; }\n}","typeGuard":"const isValidCursor = (c: string | undefined | null): boolean =>\n  !!c && /^[A-Za-z0-9_-]+$/.test(c);","tryCatchPattern":"try { page = await api.search({ cursor }); } catch (e) { if (e.message === 'Invalid cursor') { page = await api.search({}); } else throw e; }","preventionTips":["Pass cursor strings through unmodified (preserve URL encoding)","Never reuse cursors across API-version upgrades","On any 'Invalid cursor' 400, restart pagination from the first page"],"tags":["pagination","cursor","validation","api"],"backgroundTag":"invalid-argument-format","analyzedSha":"e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}