RocketChat/Rocket.Chat · error · Error
room-closed
Error message
room-closed
What it means
POST /api/v1/livechat/message rejects with 'room-closed' when the token and rid resolve to a room whose `open` flag is false. The conversation was already closed (agent closed it, visitor closed it via Omnichannel_allow_visitors_to_close_conversation, or auto-close/inactivity) and the server refuses new messages on closed rooms.
Solutions
- React to the chat-closed callback (widget receives onChatClosed / the finish event) and stop further sends; show the finished/registration state instead.
- If the visitor should continue, start a new chat (POST /api/v1/livechat/room with the token) and send to the new rid.
- If closing was unintended, the agent can reopen the conversation, after which sends succeed.
Example fix
// before
widget.on('send', (msg) => postMessage(rid, msg)); // fires after close -> room-closed
// after
let open = true;
widget.on('chat-closed', () => { open = false; });
widget.on('send', async (msg) => {
if (!open) {
({ rid } = await post('/api/v1/livechat/room', { token })); // new conversation
open = true;
}
await postMessage(rid, msg);
}); Defensive patterns
Strategy: validation
Validate before calling
// Track room open state and re-open before sending
let roomOpen = true;
onChatClosed(() => { roomOpen = false; });
if (!roomOpen) {
const { room } = await post('/api/v1/livechat/room', { token });
rid = room._id;
roomOpen = true;
}
await post('/api/v1/livechat/message', { token, rid, msg }); Type guard
function isOpenRoom(room: unknown): room is { open: true; _id: string } {
return typeof room === 'object' && room !== null && (room as any).open === true;
} Try / catch
try { await postMessage(token, rid, msg); } catch (e) { if (e.message !== 'room-closed') throw e; const { room } = await post('/api/v1/livechat/room', { token }); rid = room._id; await postMessage(token, rid, msg); } Prevention
- Handle the chat-closed/finish callback and flip UI to a finished state that blocks sends.
- Disable the composer immediately when the close event arrives to avoid in-flight races.
- Treat room-closed as a signal to start a new conversation, never as a retry-the-same-request error.
When it happens
Trigger: Widget sends a message queued offline after the conversation ended; race where the close event arrives while a send is in flight; integration posting to a rid captured before the chat was closed.
Common situations: Visitor types a message just as the agent closes the chat; widget state not updated on the chat-close callback; automated flows posting to closed rooms during off-hours.
Related errors
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/826742716d8d3e5f.
Report an issue: GitHub.
Appendix: source
Thrown at apps/meteor/server/api/v1/omnichannel/message.ts:42
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: {
_id,
rid,
msg,
token,View on GitHub (pinned to b2c16d5842)