toeverything/AFFiNE · warning · BadRequest

Invalid pagination cursor

Error message

Invalid pagination cursor

What it means

Thrown by parseJsonCursor (workspace-analytics.ts:204) when the `after` cursor string is neither parseable by JSON.parse nor by the project's decodeWithJson helper. All analytics list endpoints that paginate by cursor pass through this function. UserFriendlyError BadRequest, code bad_request, HTTP 400.

Source

Thrown at packages/backend/server/src/models/workspace-analytics.ts:204

}

function parseJsonCursor<T>(cursor?: string | null): T | null {
  if (!cursor) {
    return null;
  }

  const raw = cursor.trim();
  if (!raw) {
    return null;
  }

  try {
    return JSON.parse(raw) as T;
  } catch {
    try {
      return decodeWithJson<T>(raw);
    } catch {
      throw new BadRequest('Invalid pagination cursor');
    }
  }
}

function parseCursorDate(value: unknown): Date {
  if (
    typeof value !== 'string' &&
    typeof value !== 'number' &&
    !(value instanceof Date)
  ) {
    throw new BadRequest('Invalid pagination cursor');
  }

  const parsed = new Date(value);
  if (Number.isNaN(parsed.getTime())) {
    throw new BadRequest('Invalid pagination cursor');
  }
  return parsed;

View on GitHub (pinned to 26c515e050)

Solutions

  1. Drop the cursor and request the first page (omit the `after` parameter).
  2. Re-encode the cursor using the server's encode helper.
  3. Treat cursors as opaque tokens and never construct or modify them client-side.

Example fix

// before
GET /analytics?after=%%invalid
// after
GET /analytics   // omit `after` to start from page 1
Defensive patterns

Strategy: validation

Validate before calling

function safeCursor(raw: string | null | undefined): string | null {
  if (!raw) return null;
  const s = raw.trim();
  if (!s) return null;
  try { JSON.parse(s); return s; } catch {}
  try { decodeWithJson(s); return s; } catch {}
  return null; // drop invalid cursor -> start from page 1
}

Try / catch

import { BadRequest } from '../base/error/errors.gen';

try {
  const cursor = parseJsonCursor(pagination.after);
} catch (e) {
  if (e instanceof BadRequest) {
    // drop the cursor and request the first page
  } else throw e;
}

Prevention

When it happens

Trigger: Sending a malformed, truncated, or hand-edited `after` query parameter to any paginated analytics endpoint.

Common situations: Frontend cached an old cursor format across a server version upgrade; a proxy truncated the query string; a bookmarked URL contains a cursor that is no longer valid; client treated the cursor as editable.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/8aadfa0363bffcaf. Report an issue: GitHub.