RocketChat/Rocket.Chat · error · Error

error-invalid-cursor

error-invalid-cursor

Error message

error-invalid-cursor

What it means

decodeHistoryCursor throws error-invalid-cursor when the cursor string passed to loadRoomHistory's latest/oldest parameters is not composed purely of digits. The cursor is an epoch-milliseconds timestamp produced by encodeHistoryCursor, so any non-numeric input (UUID, base64 offset cursor, 'null', empty string) is rejected. This guards the room history pagination API against malformed or foreign cursor formats.

Source

Thrown at apps/meteor/server/lib/messages/loadRoomHistory.ts:30

export type RoomHistoryCursor = {
	next: string | null;
	previous: string | null;
};

export type RoomHistoryResult = {
	messages: IMessage[];
	cursor: RoomHistoryCursor;
	firstUnread?: IMessage;
	unreadNotLoaded?: number;
};

export function encodeHistoryCursor(ts: Date): string {
	return `${ts.getTime()}`;
}

export function decodeHistoryCursor(cursor: string): Date {
	if (!/^\d+$/.test(cursor)) {
		throw new Error('error-invalid-cursor');
	}

	const date = new Date(parseInt(cursor, 10));

	if (date.toString() === 'Invalid Date') {
		throw new Error('error-invalid-cursor');
	}

	return date;
}

/**
 * Cursor-paginated room history, ordered newest-first regardless of paging direction.
 *
 * `lastSeen` positions the unread divider only. Using it as a pagination bound instead would truncate
 * the page at the marker, which is why the per-type `*.history` endpoints cannot serve this.
 */
export async function loadRoomHistory({

View on GitHub (pinned to b263243745)

Solutions

  1. Echo back exactly the cursor string returned by the previous loadRoomHistory response instead of constructing one
  2. Ensure latest/oldest are numeric epoch-millisecond strings (e.g. String(Date.now()))
  3. Sanitize client-side: strip or reject non-digit cursor values before calling the method
  4. If migrating from an older offset-based pagination, map old offsets to timestamps server-side instead of passing them through

Example fix

// before
Meteor.call('loadRoomHistory', { rid, latest: 'page=2' });

// after
Meteor.call('loadRoomHistory', { rid, latest: String(Date.now()) });
Defensive patterns

Strategy: validation

Validate before calling

const isValidCursor = (c: unknown): c is string => typeof c === 'string' && /^\d+$/.test(c) && Number(c) <= 8.64e15;
if (latest != null && !isValidCursor(latest)) throw new TypeError('latest must be an epoch-ms digit string');

Type guard

const isHistoryCursor = (c: unknown): c is string => typeof c === 'string' && /^\d+$/.test(c) && Number(c) <= 8.64e15;

Prevention

When it happens

Trigger: Calling the loadRoomHistory meteor method (or the REST endpoint that maps to it) with a latest/oldest value that fails /^\d+$/ — e.g. latest='abc', latest='', latest='1690000000000abc', or passing an offset-style cursor from another endpoint.

Common situations: Mixing cursor formats between API versions, forwarding a cursor from a different service, client sending null/undefined stringified as 'null' or 'undefined', or hand-crafting cursors instead of echoing back the value returned by a previous history response.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b263243745 (2026-08-28). Data as JSON: /api/errors/53f8a5b4c27493c3. Report an issue: GitHub.