RocketChat/Rocket.Chat · error · Meteor.Error

error-cursor-and-lastUpdate-conflict

error-cursor-and-lastUpdate-conflict

Error message

The attributes "next", "previous" and "lastUpdate" cannot be used together

What it means

Thrown by the 'messages/get' Meteor method (getMessageHistory in apps/meteor/server/publications/messages.ts) when cursor pagination parameters ('next' or 'previous') are combined with 'lastUpdate'. The API supports three mutually exclusive modes: plain channel history, lastUpdate-based sync, and cursor pagination. Mixing the sync mode with cursor pagination is rejected because the server would not know which query shape to build.

Solutions

  1. Remove 'lastUpdate' from the payload when using 'next'/'previous' cursor pagination
  2. Remove 'next'/'previous' when you want the lastUpdate sync mode
  3. Audit shared request builders so stale fields from the other mode are not forwarded

Example fix

// before
Meteor.call('messages/get', rid, { next: cursor, type: 'UPDATED', lastUpdate, count: 50 });

// after (cursor pagination only)
Meteor.call('messages/get', rid, { next: cursor, type: 'UPDATED', count: 50 });
Defensive patterns

Strategy: validation

Validate before calling

function assertMessagesGetParams(p: { next?: string; previous?: string; lastUpdate?: Date; type?: string; fromTs?: Date }) {
  const hasCursor = Boolean(p.next || p.previous);
  if (hasCursor && p.lastUpdate) throw new Error('cursor params cannot be combined with lastUpdate');
  if (p.next && p.previous) throw new Error('pass only one of next/previous');
  if (p.fromTs && !p.lastUpdate) throw new Error('fromTs requires lastUpdate');
}

Type guard

type CursorParams = { next?: string; previous?: string; type: 'UPDATED' | 'DELETED'; lastUpdate?: undefined; fromTs?: undefined };
type SyncParams = { lastUpdate: Date; fromTs?: Date; next?: undefined; previous?: undefined; type?: undefined };
function isCursorParams(p: CursorParams | SyncParams): p is CursorParams {
  return Boolean((p.next || p.previous) && p.type);
}

Try / catch

try { await Meteor.callAsync('messages/get', rid, params); } catch (e) { if (e.error === 'error-cursor-and-lastUpdate-conflict') { /* strip lastUpdate and retry in cursor mode */ } }

Prevention

When it happens

Trigger: Calling Meteor.call('messages/get', rid, { next: '<cursor>', type: 'UPDATED', lastUpdate: new Date() }) — the same payload with 'previous' instead of 'next' triggers it as well. Also triggered when a client migrates from the old lastUpdate sync loop to cursor pagination but forgets to strip the old parameter.

Common situations: Upgrading a chat client to cursor-based history loading while keeping the legacy lastUpdate refresh payload; copy-pasting a sync payload and adding a cursor from a previous response; REST/g RPC wrappers that forward all query params verbatim.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/16ae39dffce5579f. Report an issue: GitHub.

Appendix: source

Thrown at apps/meteor/server/publications/messages.ts:257

> => {
	if (!(await canAccessRoomIdAsync(rid, fromId))) {
		throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'messages/get' });
	}

	if (type && !['UPDATED', 'DELETED'].includes(type)) {
		throw new Meteor.Error('error-type-param-not-supported', 'The "type" parameter must be either "UPDATED" or "DELETED"');
	}

	if ((next || previous) && !type) {
		throw new Meteor.Error('error-type-param-required', 'The "type" parameter is required when using the "next" or "previous" parameters');
	}

	if (next && previous) {
		throw new Meteor.Error('error-cursor-conflict', 'You cannot provide both "next" and "previous" parameters');
	}

	if ((next || previous) && lastUpdate) {
		throw new Meteor.Error(
			'error-cursor-and-lastUpdate-conflict',
			'The attributes "next", "previous" and "lastUpdate" cannot be used together',
		);
	}

	// `fromTs` only bounds the query on the `lastUpdate` path; neither cursor pagination nor the channel
	// history fallback honors it, so accepting it there would silently widen the result set.
	if (fromTs && !lastUpdate) {
		throw new Meteor.Error('error-fromTs-requires-lastUpdate', 'The "fromTs" parameter can only be used together with "lastUpdate"');
	}

	const hasCursorPagination = !!((next || previous) && count !== null && type);

	if (!hasCursorPagination && !lastUpdate) {
		return getChannelHistory({ rid, fromUserId: fromId, latest: latestDate, oldest: oldestDate, inclusive, count, unreads });
	}

	if (lastUpdate) {

View on GitHub (pinned to b2c16d5842)