mastra-ai/mastra · error

page must be >= 0

Error message

page must be >= 0

What it means

listMessages validates that the page argument is zero or a positive number before computing an offset. Negative pages have no meaning in offset-based pagination, so the in-memory storage domain throws immediately. This is a fail-fast argument validation, not a state error.

Source

Thrown at packages/core/src/storage/domains/memory/inmemory.ts:133

    orderBy,
  }: StorageListMessagesInput): Promise<StorageListMessagesOutput> {
    const metadataFilter = validateStorageMetadataFilter(filter?.metadata);
    // Normalize threadId to array
    const threadIds = Array.isArray(threadId) ? threadId : [threadId];

    if (threadIds.length === 0 || threadIds.some(id => !id.trim())) {
      throw new Error('threadId must be a non-empty string or array of non-empty strings');
    }

    const threadIdSet = new Set(threadIds);

    const { field, direction } = this.parseOrderBy(orderBy, 'ASC');

    // Normalize perPage for query (false → MAX_SAFE_INTEGER, 0 → 0, undefined → 40)
    const perPage = normalizePerPage(perPageInput, 40);

    if (page < 0) {
      throw new Error('page must be >= 0');
    }

    // Prevent unreasonably large page values that could cause performance issues
    const maxOffset = Number.MAX_SAFE_INTEGER / 2;
    if (page * perPage > maxOffset) {
      throw new Error('page value too large');
    }

    // Calculate offset from page
    const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);

    // When perPage is 0 with no includes, there's nothing to return.
    if (perPage === 0 && (!include || include.length === 0)) {
      return { messages: [], total: 0, page, perPage: perPageForResponse, hasMore: false };
    }

    // Step 1: Get messages matching threadId(s) and optionally resourceId
    let threadMessages = Array.from(this.db.messages.values()).filter((msg: any) => {

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Clamp page before calling: Math.max(0, page).
  2. Validate user/URL-supplied page numbers at the boundary (NaN check plus >= 0) before invoking storage.
  3. If iterating pages in a loop, start at 0 and guard decrement logic against going below zero.

Example fix

// before
const page = Number(searchParams.get('page'));
await storage.listMessages({ threadId, page });
// after
const page = Math.max(0, Number.parseInt(searchParams.get('page') ?? '0', 10) || 0);
await storage.listMessages({ threadId, page });
Defensive patterns

Strategy: validation

Validate before calling

function normalizePage(raw) {
  const n = Number(raw);
  return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
}
await storage.listMessages({ threadId, page: normalizePage(rawPage) });

Type guard

function isValidPage(v: unknown): v is number {
  return typeof v === 'number' && Number.isFinite(v) && Number.isInteger(v) && v >= 0;
}

Prevention

When it happens

Trigger: Calling listMessages({ threadId, page: -1 }) or any negative page, typically from a decrementing page counter that goes below 0, or from user-supplied query params parsed with parseInt without clamping.

Common situations: UI 'previous page' buttons decrementing past the first page; URL query strings like ?page=-1; Math.ceil/floor rounding producing -0-adjacent negatives on empty datasets; hand-rolled cursors.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/5364b2835d434b57. Report an issue: GitHub.