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
- Echo back exactly the cursor string returned by the previous loadRoomHistory response instead of constructing one
- Ensure latest/oldest are numeric epoch-millisecond strings (e.g. String(Date.now()))
- Sanitize client-side: strip or reject non-digit cursor values before calling the method
- 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
- Only echo cursors returned by previous loadRoomHistory responses
- Standardize on epoch-milliseconds strings for all cursor values
- Validate cursor format and numeric range before sending requests
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
- error-cursor-conflict
- error-invalid-sort
- duplicated-account
- error-invalid-account
- error-user-registration-custom-field
AI-assisted analysis of RocketChat/Rocket.Chat@b263243745 (2026-08-28).
Data as JSON: /api/errors/53f8a5b4c27493c3.
Report an issue: GitHub.