RocketChat/Rocket.Chat · error · Meteor.Error

error-invalid-update-key

error-invalid-update-key

Error message

Cannot update the message ${key}

What it means

executeUpdateMessage iterates every key of the submitted message patch and throws error-invalid-update-key when a key is outside allowedEditedFields AND its value differs from the stored message. Allowed keys are: tshow, alias, attachments, avatar, emoji, msg, customFields, content, e2eMentions. Echoing back unchanged protected fields (u, ts, rid, mentions, _id, ...) is tolerated; changing them is rejected, with the offending key named in the message.

Source

Thrown at apps/meteor/server/meteor-methods/messages/updateMessage.ts:28

import { applyAirGappedRestrictionsValidation } from '../../lib/cloud/license/airGappedRestrictionsWrapper';
import { updateMessage } from '../../lib/messages/updateMessage';
import { settings } from '../../settings';

const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields', 'content', 'e2eMentions'];

export async function executeUpdateMessage(
	uid: IUser['_id'],
	message: AtLeast<IMessage, '_id' | 'rid' | 'msg' | 'customFields'> | AtLeast<IMessage, '_id' | 'rid' | 'content'>,
	previewUrls?: string[],
) {
	const originalMessage = await Messages.findOneById(message._id);
	if (!originalMessage?._id) {
		return;
	}

	Object.entries(message).forEach(([key, value]) => {
		if (!allowedEditedFields.includes(key) && value !== originalMessage[key as keyof IMessage]) {
			throw new Meteor.Error('error-invalid-update-key', `Cannot update the message ${key}`, {
				method: 'updateMessage',
			});
		}
	});

	// IF the message has custom fields, always update
	// Ideally, we'll compare the custom fields to check for change, but since we don't know the shape of
	// custom fields, as it's user defined, we're gonna update
	const msgText = originalMessage?.attachments?.[0]?.description ?? originalMessage.msg;
	if (msgText === message.msg && !previewUrls && !message.customFields) {
		return;
	}

	if (!!message.tmid && originalMessage._id === message.tmid) {
		throw new Meteor.Error('error-message-same-as-tmid', 'Cannot set tmid the same as the _id', {
			method: 'updateMessage',
		});
	}

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Send only the editable subset: { _id, rid, msg?, customFields?, attachments?, alias?, avatar?, emoji?, tshow?, content?, e2eMentions? }
  2. Strip the patch through an allowlist picker before Meteor.call('updateMessage', ...)
  3. Use the dedicated APIs for other mutations (pinning, reactions, moderation) instead of updateMessage
  4. Ensure values that merely pass through (unchanged fields) are sent byte-identical or omitted, since deep-equality is what spares them

Example fix

// before: whole message object echoed back
await Meteor.callAsync('updateMessage', { ...messageFromStore, msg: newMsg });

// after: minimal editable patch
await Meteor.callAsync('updateMessage', {
	_id: messageFromStore._id,
	rid: messageFromStore.rid,
	msg: newMsg,
});
Defensive patterns

Strategy: type-guard

Type guard

const ALLOWED_EDIT_FIELDS = [
	'tshow',
	'alias',
	'attachments',
	'avatar',
	'emoji',
	'msg',
	'customFields',
	'content',
	'e2eMentions',
] as const;

type EditableMessagePatch = {
	_id: string;
	rid: string;
} & Partial<Record<(typeof ALLOWED_EDIT_FIELDS)[number], unknown>>;

const toEditablePatch = (message: Record<string, unknown>): EditableMessagePatch =>
	Object.fromEntries(
		Object.entries(message).filter(
			([key]) => key === '_id' || key === 'rid' || (ALLOWED_EDIT_FIELDS as readonly string[]).includes(key),
		),
	) as EditableMessagePatch;

// type guard
const isEditablePatch = (m: Record<string, unknown>): m is EditableMessagePatch =>
	Object.keys(m).every(
		(k) => k === '_id' || k === 'rid' || (ALLOWED_EDIT_FIELDS as readonly string[]).includes(k),
	);

Try / catch

try {
	await Meteor.callAsync('updateMessage', patch);
} catch (e: any) {
	if (e?.error === 'error-invalid-update-key') {
		// e.reason names the offending key: strip it and resend the reduced patch
		const badKey = /message (.+)$/.exec(e.reason ?? '')?.[1];
		if (badKey) delete patch[badKey];
	}
	throw e;
}

Prevention

When it happens

Trigger: Client sends the whole message object from the UI with a mutated protected field (e.g. ts or mentions changed by client logic); integration attempts to change the author (u) or timestamp via updateMessage; a diffing bug that copies the full document into the patch; trying to move a message by editing rid.

Common situations: Message-edit forms binding the full message model instead of an { _id, rid, msg } patch; bots trying to rewrite history; client normalization (e.g. date parsing turning ts into a new object identity) counting as a value change.

Related errors


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