RocketChat/Rocket.Chat · error · Meteor.Error
error-invalid-message
error-invalid-message
Error message
Invalid message
What it means
getThreadMessages (deprecated in 9.0.0 in favor of GET /v1/chat.getThreadMessages) throws error-invalid-message when its tmid parameter is not a string — a manual typeof guard that runs before any database access. It does NOT mean 'thread not found': a well-typed but unknown tmid returns an empty array (getThreadMessages.ts:41-44 returns [] when the thread message is missing). Hitting it means the client sent undefined, null, a number, or an object where the thread-parent message _id string was expected.
Source
Thrown at apps/meteor/server/meteor-methods/messages/getThreadMessages.ts:38
Meteor.methods<ServerMethods>({
async getThreadMessages({ tmid, limit, skip }) {
methodDeprecationLogger.method('getThreadMessages', '9.0.0', '/v1/chat.getThreadMessages');
if ((limit ?? 0) > MAX_LIMIT) {
throw new Meteor.Error('error-not-allowed', `max limit: ${MAX_LIMIT}`, {
method: 'getThreadMessages',
});
}
if (!Meteor.userId() || !settings.get('Threads_enabled')) {
throw new Meteor.Error('error-not-allowed', 'Threads Disabled', {
method: 'getThreadMessages',
});
}
if (typeof tmid !== 'string') {
throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'getThreadMessages' });
}
const thread = await Messages.findOneById(tmid);
if (!thread) {
return [];
}
const user = await Meteor.userAsync();
const room = await Rooms.findOneById(thread.rid);
if (!user || !room || !(await canAccessRoomAsync(room, user))) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getThreadMessages' });
}
if (!thread.tcount) {
return [];
}
View on GitHub (pinned to 2a7de45707)
Solutions
- Pass the thread-parent message's _id as a non-empty string: for a reply message use its tmid field (it points at the root); for the root message use its _id.
- Guard the call site: only invoke the method when typeof tmid === 'string' && tmid.length > 0.
- Migrate to the REST endpoint GET /v1/chat.getThreadMessages — the DDP method is deprecated for removal in 9.0.0.
- If tmid originates from a URL/query parameter, validate and coerce it to a string before it reaches the method call.
Example fix
// before — `message` is the thread ROOT: it has no tmid of its own,
// so tmid is undefined and the server throws error-invalid-message
await Meteor.callAsync('getThreadMessages', { tmid: message.tmid });
// after — root: use its _id; reply: use its tmid (which points at the root)
await Meteor.callAsync('getThreadMessages', { tmid: message.tmid ?? message._id }); Defensive patterns
Strategy: validation
Validate before calling
const isThreadParentId = (value: unknown): value is IMessage['_id'] =>
typeof value === 'string' && value.length > 0;
if (!isThreadParentId(tmid)) {
throw new TypeError('tmid must be the thread-parent message _id (non-empty string)');
}
const messages = await Meteor.callAsync('getThreadMessages', { tmid, limit: 50 }); Type guard
const isThreadParentId = (value: unknown): value is IMessage['_id'] => typeof value === 'string' && value.length > 0;
Try / catch
try {
const messages = await Meteor.callAsync('getThreadMessages', { tmid });
} catch (error) {
if (error instanceof Meteor.Error && error.error === 'error-invalid-message') {
// deterministic bad input — log loudly and fix the caller; do NOT retry.
// note: a valid but unknown tmid returns [] instead of throwing
}
throw error;
} Prevention
- Derive tmid from a reply's tmid field or the root's _id — thread ROOT messages have no tmid of their own.
- Validate the whole params object before any DDP call; these methods use manual typeof checks, not meteor/check boundary validation.
- Never retry this error — it is deterministic bad input, not a transient failure.
- Reserve runtime error handling for error-not-allowed (access/threads-disabled); type errors should be impossible by construction.
When it happens
Trigger: Meteor.call('getThreadMessages', { tmid }) where tmid is undefined (e.g., reading .tmid off a thread ROOT message, which has no tmid of its own — only replies carry tmid), null, a numeric id, or an entire message object instead of message._id/message.tmid. The check runs after the limit check (limit > 100 throws first) and the Threads_enabled/auth check, so it only fires for logged-in users on a threads-enabled workspace with a valid limit.
Common situations: Client refactors renaming fields (tmid vs _id), optional-chaining bugs like message?.tmid on the root message itself, or passing a deserialized document where its id was expected. Also hit by integrations assuming meteor/check-style argument validation — this method validates with manual typeof checks, so malformed input reaches the method instead of being rejected at the boundary.
Related errors
AI-assisted analysis of RocketChat/Rocket.Chat@2a7de45707 (2026-08-21).
Data as JSON: /api/errors/247678ed70832202.
Report an issue: GitHub.