immich-app/immich · error · BadRequestException

Invalid cursor

Error message

Invalid cursor

What it means

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.

Solutions

  1. Request the first page by omitting the cursor parameter (server returns { offset: 0 } equivalent) and use a fresh cursor from the previous response.
  2. Ensure the cursor is transmitted verbatim (URL-encoded) — do not trim, decode, or re-encode it.
  3. If cursors persist across a server upgrade, discard stored cursors and restart pagination.
  4. Client-side: wrap cursor usage so any 400 'Invalid cursor' restarts pagination from page one.

Example fix

// before
const page = await api.search({ cursor: cursor.slice(0, 20) });
// after
const page = await api.search({ cursor }); // pass cursor unmodified, or omit for page 1
Defensive patterns

Strategy: try-catch

Validate before calling

function isPlausibleCursor(c) {
  try { return !!JSON.parse(Buffer.from(c, 'base64url').toString('utf8')).hasOwnProperty('offset'); } catch { return false; }
}

Type guard

const isValidCursor = (c: string | undefined | null): boolean =>
  !!c && /^[A-Za-z0-9_-]+$/.test(c);

Try / catch

try { page = await api.search({ cursor }); } catch (e) { if (e.message === 'Invalid cursor') { page = await api.search({}); } else throw e; }

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/652fdbef6934dc43. Report an issue: GitHub.

Appendix: source

Thrown at server/src/utils/search-cursor.ts:19

import { BadRequestException } from '@nestjs/common';
import z from 'zod';

const SearchCursorPayloadSchema = z.object({
  offset: z.int().min(0),
});

export const encodeSearchCursor = (offset: number): string =>
  Buffer.from(JSON.stringify({ offset } satisfies z.infer<typeof SearchCursorPayloadSchema>)).toString('base64url');

export const decodeSearchCursor = (cursor?: string): { offset: number } => {
  if (cursor === undefined) {
    return { offset: 0 };
  }

  try {
    return SearchCursorPayloadSchema.parse(JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8')));
  } catch {
    throw new BadRequestException('Invalid cursor');
  }
};

View on GitHub (pinned to e55ac299a4)