RocketChat/Rocket.Chat · error · Meteor.Error

error-invalid-message

error-invalid-message

Error message

Invalid Message

What it means

Thrown by POST chat.readThread (apps/meteor/server/api/v1/chat.ts:289) when Messages.findOneById(tmid) returns null or a document without an rid. The endpoint needs the thread's parent message to resolve which room the thread lives in, so an unknown or malformed tmid fails here with error-invalid-message. Note the schema only requires tmid to be a non-empty string, so any well-formed but wrong id reaches this check.

Solutions

  1. Verify the id exists and is a thread parent first: GET /api/v1/chat.getMessage?msgId=<tmid> must return a message (and its rid) before you mark the thread read.
  2. Confirm you are passing the thread's main message id (the message other replies reference via tmid), not a reply's _id and not a room id.
  3. If the parent was deleted, drop the unread state locally - there is nothing to mark read.
  4. Add a 404-style branch in your client for error-invalid-message so stale ids are discarded instead of retried.

Example fix

// before
await rc.post('/api/v1/chat.readThread', { tmid: cachedTmid });
// error-invalid-message when the parent message is gone

// after
try {
  await rc.get('/api/v1/chat.getMessage', { params: { msgId: cachedTmid } });
  await rc.post('/api/v1/chat.readThread', { tmid: cachedTmid });
} catch (e) {
  if (e?.errorType === 'error-invalid-message') forgetThread(cachedTmid);
}
Defensive patterns

Strategy: validation

Validate before calling

const res = await rc.get('/api/v1/chat.getMessage', { params: { msgId: tmid } });
if (!res?.message?.rid) throw new Error('tmid is not a valid thread parent');
await rc.post('/api/v1/chat.readThread', { tmid });

Type guard

const isValidThreadParent = (m: { rid?: string } | null | undefined): m is { rid: string } =>
  typeof m?.rid === 'string' && m.rid.length > 0;

Try / catch

try {
  await rc.post('/api/v1/chat.readThread', { tmid });
} catch (e) {
  if (e?.errorType === 'error-invalid-message') discardThread(tmid); // gone, stop syncing
  else throw e;
}

Prevention

When it happens

Trigger: POST /api/v1/chat.readThread with a tmid that (a) does not exist in the Messages collection (typo, stale id from another workspace, deleted thread parent), (b) belongs to a message whose rid field is missing, or (c) was confused with a thread reply id or a room id. Threads must be enabled or you get error 320 first.

Common situations: Bots persisting message ids across workspace re-imports/migrations where ids change; clients resending tmid after the parent message was purged by a retention policy; passing msgId of a reply instead of the thread's main message; copy-paste truncation of the 17-char Mongo id.

Related errors


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

Appendix: source

Thrown at apps/meteor/server/api/v1/chat.ts:293

							type: 'boolean',
							enum: [true],
						},
					},
					required: ['success'],
					additionalProperties: false,
				}),
			},
		},
		async function action() {
			if (!settings.get<boolean>('Threads_enabled')) {
				throw new Meteor.Error('error-not-allowed', 'Threads Disabled');
			}

			const { tmid } = this.bodyParams;

			const thread = await Messages.findOneById(tmid, { projection: { rid: 1 } });
			if (!thread?.rid) {
				throw new Meteor.Error('error-invalid-message', 'Invalid Message');
			}

			const [user, room] = await Promise.all([
				Users.findOneById(this.userId),
				Rooms.findOneById(thread.rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } }),
			]);

			if (!room) {
				throw new Meteor.Error('error-room-does-not-exist', 'This room does not exist');
			}

			if (!user || !(await canAccessRoomAsync(room, user))) {
				throw new Meteor.Error('error-not-allowed', 'Not Allowed');
			}

			await callbacks.run('beforeReadMessages', room._id, user._id);
			await readThread({ user, room, tmid });

View on GitHub (pinned to e4b8178b20)