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
- 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.
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
- 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.
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
- assetIds, albumId, or userId is required
- Invalid job name
- Library ${id} not found
- Invalid import path: ${path.message}
- Notification not found
AI-assisted analysis of immich-app/immich@5666d57f15 (2026-09-01).
Data as JSON: /api/errors/652fdbef6934dc43.
Report an issue: GitHub.