{"record":{"id":"652fdbef6934dc43","repo":"immich-app/immich","slug":"invalid-cursor","errorCode":null,"errorMessage":"Invalid cursor","messagePattern":"Invalid cursor","errorType":"validation","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","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/5666d57f15a66bd5518119c5d9f4d2b62f3a86c1/server/src/utils/search-cursor.ts#L1-L22","documentation":"`decodeSearchCursor` decodes an opaque base64url cursor into a validated payload (via SearchCursorPayloadSchema.parse). If the cursor is not valid base64url, not valid JSON, or fails schema validation, it throws BadRequestException('Invalid cursor'). The cursor contents are intentionally opaque, so any tampering, truncation, or version mismatch results in this error.","triggerScenarios":"Client sends a cursor string that was truncated/corrupted (URL encoding stripped, copied incompletely); cursor from a different API version whose schema no longer matches; hand-crafted or tampered cursor values; sending an empty/garbage string where a cursor is expected.","commonSituations":"Frontend double-encodes or decodes the cursor in the URL; bookmarks/links saved from an older server version; proxies mangling base64url characters; users editing query params manually; upgrading immich so old saved cursors no longer validate.","solutions":["Retry the request without the cursor parameter — this restarts pagination from the first page and returns a fresh cursor.","Fix the client to pass the cursor through opaquely (no decode/re-encode, no truncation) in the query string.","Clear stale saved links/bookmarks from older versions and re-run the search.","If cursors must survive upgrades, keep the payload schema backward compatible.","Verify URL encoding: base64url is URL-safe, but ensure no additional encoding layers corrupt the value."],"exampleFix":"// before\nfetch(`/search?query=x&cursor=${decodeURIComponent(cursor)}`); // corrupts cursor\n// after\nfetch(`/search?query=x&cursor=${encodeURIComponent(cursor)}`); // pass through opaquely","handlingStrategy":"validation","validationCode":"// validate cursor shape before sending\nconst isCursor = (c: unknown): c is string =>\n  typeof c === 'string' && c.length > 0 && /^[A-Za-z0-9_-]+$/.test(c);\nif (!isCursor(cursor)) cursor = undefined; // omit and start from page 1","typeGuard":"function isValidCursor(c: unknown): c is string {\n  return typeof c === 'string' && c.length > 0 && /^[A-Za-z0-9_-]+$/.test(c);\n}","tryCatchPattern":"try {\n  const page = await search({ query, cursor });\n} catch (e) {\n  if (e instanceof BadRequestException && e.response.includes('Invalid cursor')) {\n    cursor = undefined; // restart pagination from the first page\n    const page = await search({ query });\n  } else throw e;\n}","preventionTips":["Pass cursors opaquely: no decoding, re-encoding, or truncation on the client.","URL-encode the cursor exactly once when placing it in a query string.","Drop the cursor param and retry from page 1 on this error — it is always client-supplied data.","Don't persist cursors across server version upgrades; schemas can change.","Never construct cursor values by hand."],"tags":["pagination","cursor","validation","bad-request","api"],"backgroundTag":"invalid-pagination-cursor","analyzedSha":"5666d57f15a66bd5518119c5d9f4d2b62f3a86c1","analyzedAt":"2026-09-01T05:20:49.208Z","contentChangedAt":"2026-09-01T05:20:49.208Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}