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
- Send only the editable subset: { _id, rid, msg?, customFields?, attachments?, alias?, avatar?, emoji?, tshow?, content?, e2eMentions? }
- Strip the patch through an allowlist picker before Meteor.call('updateMessage', ...)
- Use the dedicated APIs for other mutations (pinning, reactions, moderation) instead of updateMessage
- 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
- Send the minimal edit patch { _id, rid, msg } instead of echoing the full message object
- Keep an allowlist of editable fields in client code and mirror it from the server constant
- Do not client-parse dates or reassign objects on pass-through fields; any value drift counts as an edit attempt
- Use dedicated endpoints for pinning, reactions and moderation rather than updateMessage
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
- invalid-params
- error-message-ts-out-of-sync
- error-invalid-message
- error-action-not-allowed
- error-invalid-room
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/f35fc63d8b812553.
Report an issue: GitHub.