RocketChat/Rocket.Chat · error · Error
invalid-room
Error message
invalid-room
What it means
POST /api/v1/livechat/message resolves the room via findRoom(token, rid) → LivechatRooms.findOneByIdAndVisitorToken(rid, token). It returns null not only when the rid doesn't exist but also when the room exists and belongs to a different visitor token; either case throws 'invalid-room'. The check is a pairing check: rid AND token must match the same room.
Solutions
- Always take rid and token from the same flow: the response of POST /api/v1/livechat/room gives the rid bound to that visitor's token.
- If the visitor was re-registered, create a new room with the new token instead of reusing the old rid.
- Debug by fetching the room with token only (no rid) and comparing the returned room's _id to the rid you sent.
Example fix
// before: rid from an old session, token re-registered -> invalid-room
await post('/api/v1/livechat/message', { token, rid: oldRid, msg });
// after: keep token+rid as a pair
const { room } = await post('/api/v1/livechat/room', { token }); // room._id matches this token
const rid = room._id;
await post('/api/v1/livechat/message', { token, rid, msg }); Defensive patterns
Strategy: validation
Validate before calling
// Ensure rid belongs to this token before sending
const room = await getRoom(token); // resolves room by visitor token
if (!room || room._id !== rid) {
throw new Error('rid/token mismatch: re-open a room for this visitor');
}
await post('/api/v1/livechat/message', { token, rid, msg }); Type guard
function isRoomForToken(room: unknown, token: string): room is { _id: string; v: { token: string } } {
return typeof room === 'object' && room !== null && (room as any)?.v?.token === token;
} Try / catch
try { await postMessage(token, rid, msg); } catch (e) { if (e.message === 'invalid-room') { const { room } = await post('/api/v1/livechat/room', { token }); rid = room._id; await postMessage(token, rid, msg); } else throw e; } Prevention
- Remember the check is rid AND token pairing, not just existence.
- Always obtain rid from the same registration flow that issued the token.
- After re-registering a visitor, create a fresh room instead of reusing the old rid.
When it happens
Trigger: Correct rid with a token from another visitor/session (e.g. after re-registering the visitor but keeping the old rid); mistyped rid; room deleted; token/rid taken from different environments.
Common situations: Visitor re-registration produced a new token while the client kept the previous room's rid; multiple tabs/widgets sharing a rid but not a token; hand-assembled requests mixing fixture ids.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/b4b1ad0c59b0a3ed.
Report an issue: GitHub.
Appendix: source
Thrown at apps/meteor/server/api/v1/omnichannel/message.ts:38
import { settings } from '../../../settings';
import { getPaginationItems } from '../../lib/getPaginationItems';
import { isWidget } from '../../lib/isWidget';
API.v1.addRoute(
'livechat/message',
{ validateParams: isPOSTLivechatMessageParams },
{
async post() {
const { token, rid, agent, msg } = this.bodyParams;
const guest = await findGuest(token);
if (!guest) {
throw new Error('invalid-token');
}
const room = await findRoom(token, rid);
if (!room) {
throw new Error('invalid-room');
}
if (!room.open) {
throw new Error('room-closed');
}
if (
settings.get('Livechat_enable_message_character_limit') &&
msg.length > parseInt(settings.get('Livechat_message_character_limit'))
) {
throw new Error('message-length-exceeds-character-limit');
}
const _id = this.bodyParams._id || Random.id();
const messageToSend = {
guest,
message: {View on GitHub (pinned to b2c16d5842)