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
- 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.
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
- 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
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
- Asset dimensions are not available for editing
- Crop action must be the first edit action
- Crop parameters are out of bounds
- Invalid license key
- Invalid value for 'size
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)