slopus/happy · error

Invalid cursor format

Error message

Invalid cursor format

What it means

feedGet() paginates user feed items by a cursor that must have the form '0-<counter>' (a Storm/MySQL-style log sequence number prefix). A 'before' cursor that does not start with '0-' is rejected with 'Invalid cursor format' because the counter cannot be parsed for a lt comparison.

Source

Thrown at packages/happy-server/sources/app/feed/feedGet.ts:26

 * Returns items in reverse chronological order (newest first).
 * Supports cursor-based pagination using the counter field.
 */
export async function feedGet(
    tx: Tx,
    ctx: Context,
    options?: FeedOptions
): Promise<FeedResult> {
    const limit = options?.limit ?? 100;
    const cursor = options?.cursor;

    // Build where clause for cursor pagination
    const where: Prisma.UserFeedItemWhereInput = { userId: ctx.uid };

    if (cursor?.before !== undefined) {
        if (cursor.before.startsWith('0-')) {
            where.counter = { lt: parseInt(cursor.before.substring(2), 10) };
        } else {
            throw new Error('Invalid cursor format');
        }
    } else if (cursor?.after !== undefined) {
        if (cursor.after.startsWith('0-')) {
            where.counter = { gt: parseInt(cursor.after.substring(2), 10) };
        } else {
            throw new Error('Invalid cursor format');
        }
    }

    // Fetch items + 1 to determine hasMore
    const items = await tx.userFeedItem.findMany({
        where,
        orderBy: { counter: 'desc' },
        take: limit + 1
    });

    // Check if there are more items
    const hasMore = items.length > limit;

View on GitHub (pinned to b824cd0a46)

Solutions

  1. Use cursors exactly as returned by the previous page (the raw counter-based string, e.g. '0-42').
  2. Fix the client to build cursors as `0-${counter}` from the last item's counter field.
  3. Clear stale persisted pagination state and restart pagination from the beginning (omit cursor).
  4. Add client-side validation: cursor must match /^0-\d+$/ before sending.

Example fix

// before
fetchFeed({ before: lastItem.id });
// after
fetchFeed({ before: `0-${lastItem.counter}` });
Defensive patterns

Strategy: validation

Validate before calling

function isValidCursor(c) {
  return c === undefined || c.before === undefined || /^0-\d+$/.test(c.before);
}
if (!isValidCursor(cursor)) throw new Error('before cursor must be "0-<counter>"');

Type guard

function isBeforeCursor(c) {
  return typeof c === 'object' && c !== null &&
    typeof c.before === 'string' && /^0-\d+$/.test(c.before);
}

Try / catch

try {
  return await api.feed.items({ cursor });
} catch (e) {
  if (e.message === 'Invalid cursor format') {
    resetPagination();
    return await api.feed.items({});
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling the feed items query with cursor.before set to a string not starting with '0-', e.g. 'abc', '1-100', an opaque id, or an empty-but-defined string.

Common situations: Clients caching cursors from a previous backend version that used a different format; hand-constructing cursors from item ids instead of the counter; passing a base64/encoded cursor where a plain '0-N' string is expected.

Related errors


AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31). Data as JSON: /api/errors/89823232edd27021. Report an issue: GitHub.