immich-app/immich · warning · BadRequestException

Invalid cursor

Error message

Invalid cursor

What it means

`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.

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 5666d57f15)

Solutions

  1. Retry the request without the cursor parameter — this restarts pagination from the first page and returns a fresh cursor.
  2. Fix the client to pass the cursor through opaquely (no decode/re-encode, no truncation) in the query string.
  3. Clear stale saved links/bookmarks from older versions and re-run the search.
  4. If cursors must survive upgrades, keep the payload schema backward compatible.
  5. Verify URL encoding: base64url is URL-safe, but ensure no additional encoding layers corrupt the value.

Example fix

// before
fetch(`/search?query=x&cursor=${decodeURIComponent(cursor)}`); // corrupts cursor
// after
fetch(`/search?query=x&cursor=${encodeURIComponent(cursor)}`); // pass through opaquely
Defensive patterns

Strategy: validation

Validate before calling

// validate cursor shape before sending
const isCursor = (c: unknown): c is string =>
  typeof c === 'string' && c.length > 0 && /^[A-Za-z0-9_-]+$/.test(c);
if (!isCursor(cursor)) cursor = undefined; // omit and start from page 1

Type guard

function isValidCursor(c: unknown): c is string {
  return typeof c === 'string' && c.length > 0 && /^[A-Za-z0-9_-]+$/.test(c);
}

Try / catch

try {
  const page = await search({ query, cursor });
} catch (e) {
  if (e instanceof BadRequestException && e.response.includes('Invalid cursor')) {
    cursor = undefined; // restart pagination from the first page
    const page = await search({ query });
  } else throw e;
}

Prevention

When it happens

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

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

Related errors


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