RocketChat/Rocket.Chat · error · Error

error-cursor-conflict

error-cursor-conflict

Error message

error-cursor-conflict

What it means

loadRoomHistory throws error-cursor-conflict when both next and previous boolean flags are set to true simultaneously. The API supports paginating in one direction at a time; next fetches newer messages and previous fetches older messages relative to the latest/oldest cursor. Requesting both directions at once is ambiguous and rejected.

Source

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

export async function loadRoomHistory({
	userId,
	next,
	previous,
	lastSeen,
	count = 20,
	showThreadMessages = true,
	room,
}: {
	userId?: string;
	next?: string;
	previous?: string;
	lastSeen?: Date;
	count?: number;
	showThreadMessages?: boolean;
	room: IRoom;
}): Promise<RoomHistoryResult> {
	if (next && previous) {
		throw new Error('error-cursor-conflict');
	}

	const rid = room._id;

	const hiddenMessageTypes = getHiddenSystemMessages(room, settings.get<MessageTypesValues[]>('Hide_System_Messages'));

	// One extra document reveals whether a further page exists.
	const options: FindOptions<IMessage> = { sort: { ts: next ? 1 : -1 }, limit: count + 1 };

	const records = next
		? await Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes(
				rid,
				decodeHistoryCursor(next),
				FAR_FUTURE,
				hiddenMessageTypes,
				options,
				showThreadMessages,
			).toArray()

View on GitHub (pinned to b263243745)

Solutions

  1. Set only one of next or previous to true per call
  2. Refactor the caller to derive next/previous from a single direction variable, e.g. next: dir === 'next', previous: dir === 'previous'
  3. Audit option-merging code (object spreads, defaults) so a true flag isn't carried over from a previous request
  4. If you need both newer and older messages, issue two separate calls

Example fix

// before
loadRoomHistory({ room, latest, next: true, previous: true });

// after
const goingNewer = direction === 'next';
loadRoomHistory({ room, latest, next: goingNewer, previous: !goingNewer });
Defensive patterns

Strategy: type-guard

Validate before calling

if (params.next && params.previous) {
  throw new TypeError('pass next or previous, not both');
}
const { next = false, previous = false } = pickOne(params);

Type guard

const isSingleDirection = (p: { next?: boolean; previous?: boolean }): boolean => !(p.next && p.previous);

Try / catch

try {
  await loadRoomHistory(args);
} catch (err) {
  if (err instanceof Error && err.message === 'error-cursor-conflict') {
    // retry with only one direction
    await loadRoomHistory({ ...args, previous: false });
  } else throw err;
}

Prevention

When it happens

Trigger: Calling loadRoomHistory with { latest, next: true, previous: true } (or the REST equivalent with both direction params truthy), typically when a UI toggles both direction switches or when defaults merge with explicit params.

Common situations: Spread-merging pagination option objects where a stale next:true combines with a new previous:true; UI code that sets both flags 'to be safe'; refactoring a component that previously used a single direction enum into two booleans.

Related errors


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