RocketChat/Rocket.Chat · error · Error
error-invalid-room
Error message
error-invalid-room
What it means
OmnichannelInternalService.placeRoomOnHold requires the passed room to be a livechat room: it throws Error('error-invalid-room') when the room is falsy or fails isOmnichannelRoom (i.e. room.t !== 'l'). This is the entry guard that runs before all on-hold state checks (open, onHold, servedBy).
Solutions
- Filter actions to omnichannel rooms (t === 'l') before offering or invoking on-hold.
- Handle room lookup misses before calling the service so null never reaches it.
- Re-fetch the room with its t field included when building the call.
Example fix
// before
await service.placeRoomOnHold(anyRoom, comment, user); // anyRoom.t may be 'c'/'d' -> throws
// after
if (!room || !isOmnichannelRoom(room)) throw new Error('error-invalid-room');
await service.placeRoomOnHold(room, comment, user); Defensive patterns
Strategy: type-guard
Validate before calling
if (!room || !isOmnichannelRoom(room)) {
// not a livechat room (t !== 'l'); do not offer or invoke placeRoomOnHold
} Type guard
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
const isHoldableRoom = (room: unknown): room is Pick<IOmnichannelRoom, '_id' | 't' | 'open' | 'onHold'> =>
typeof room === 'object' && room !== null && isOmnichannelRoom(room); Try / catch
try {
await service.placeRoomOnHold(room, comment, user);
} catch (e: any) {
if (e?.message === 'error-invalid-room') return badRequest('Room is not an omnichannel room');
throw e;
} Prevention
- Show on-hold actions only for rooms with t === 'l'.
- Handle room lookup misses before invoking the service so null never reaches it.
- Type room payloads as IOmnichannelRoom in omnichannel flows so non-livechat rooms fail at compile time.
When it happens
Trigger: Calling placeRoomOnHold with a non-omnichannel room object - a channel, direct message, or team room (t is 'c'/'d'/'p'/'t') - or with null/undefined, e.g. wiring a generic rooms-list action to the on-hold flow without filtering by type, or not handling a lookup miss.
Common situations: Generic room action handlers that show 'place on hold' for every room type; null rooms passed after an unhandled findOneById miss; test fixtures using plain room objects without t: 'l'.
Related errors
- error-cannot-place-chat-on-hold
- error-room-already-closed
- error-room-is-already-on-hold
- error-unserved-rooms-cannot-be-placed-onhold
- AutoCloseOnHoldScheduler is not running
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/7f020e23c7adfd0b.
Report an issue: GitHub.
Appendix: source
Thrown at apps/meteor/ee/server/local-services/omnichannel.internalService.ts:41
logger: Logger;
constructor() {
super();
this.logger = new Logger('OmnichannelEE');
}
async placeRoomOnHold(
room: Pick<IOmnichannelRoom, '_id' | 't' | 'open' | 'onHold'>,
comment: string,
onHoldBy: Pick<IUser, '_id' | 'username' | 'name'>,
) {
this.logger.debug({ msg: 'Attempting to place room on hold', roomId: room._id, userId: onHoldBy?._id });
const { _id: roomId } = room;
if (!room || !isOmnichannelRoom(room)) {
throw new Error('error-invalid-room');
}
if (!room.open) {
throw new Error('error-room-already-closed');
}
if (room.onHold) {
throw new Error('error-room-is-already-on-hold');
}
const restrictedOnHold = settings.get('Livechat_allow_manual_on_hold_upon_agent_engagement_only');
const canRoomBePlacedOnHold = !room.onHold;
const canAgentPlaceOnHold = !room.lastMessage?.token;
const canPlaceChatOnHold = canRoomBePlacedOnHold && (!restrictedOnHold || canAgentPlaceOnHold);
if (!canPlaceChatOnHold) {
throw new Error('error-cannot-place-chat-on-hold');
}
if (!room.servedBy) {
throw new Error('error-unserved-rooms-cannot-be-placed-onhold');
}
View on GitHub (pinned to b2c16d5842)