RocketChat/Rocket.Chat · error · Error
room-still-open
Error message
room-still-open
What it means
requestPdfTranscript queues the Enterprise PDF-transcript job for an omnichannel room, but only for closed conversations: its first guard throws Error('room-still-open') when room.open is true. The REST route POST omnichannel/:rid/request-transcript (permission 'request-pdf-transcript', license 'livechat-enterprise') loads the room and passes it straight through, so requesting a transcript before the chat is closed fails here. The closed-state requirement exists because the PDF snapshots the finished conversation.
Solutions
- Await the room-closing flow (closeRoom / afterClose callbacks) before requesting the transcript.
- Re-fetch the room and confirm open === false immediately before calling; treat 'room-still-open' as 'defer', not 'fail'.
- In the UI, disable the request-transcript action while the room is open so users cannot trigger it early.
Example fix
// before
await closeRoom(rid, userId); // queued elsewhere, not awaited
await requestPdfTranscript(room, userId); // room.open still true -> throws
// after
const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1, v: 1, t: 1, pdfTranscriptFileId: 1 } });
if (!room || room.open) throw new Error('room-still-open');
await requestPdfTranscript(room, userId); Defensive patterns
Strategy: validation
Validate before calling
const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1, v: 1, t: 1, pdfTranscriptFileId: 1 } });
if (!room || room.open) {
// room must be closed before a transcript can be requested; defer or reject here
} Try / catch
try {
await requestPdfTranscript(room, userId);
} catch (e: any) {
if (e?.message === 'room-still-open') { scheduleRetryAfterRoomCloses(rid); return; }
throw e;
} Prevention
- Sequence operations: await the close flow before requesting the transcript.
- Disable transcript-request UI actions while the room is open.
- Always load the room fresh (with open projected) right before the call rather than reusing a cached object.
When it happens
Trigger: POST omnichannel/:rid/request-transcript while the room is still open; or server code invoking requestPdfTranscript with a room loaded before the close was persisted (close initiated but not awaited). The sendPdfTranscriptOnClose hook avoids this by running in the after-close flow.
Common situations: A client requests the transcript immediately after clicking 'close chat' without waiting for the close event; automation that closes and requests the transcript in parallel instead of in sequence; the room was reopened by a new visitor message between close and transcript request.
Related errors
- improper-room-state
- error-room-already-closed
- error-room-is-already-on-hold
- error-unserved-rooms-cannot-be-placed-onhold
- error-action-not-allowed
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/faaa4d9b5ec2434c.
Report an issue: GitHub.
Appendix: source
Thrown at apps/meteor/ee/server/lib/omnichannel/requestPdfTranscript.ts:17
import { OmnichannelTranscript, QueueWorker } from '@rocket.chat/core-services';
import type { AtLeast, IOmnichannelRoom } from '@rocket.chat/core-typings';
import ExpiryMap from 'expiry-map';
import { logger } from './logger';
// Allow to request a transcript again after 15 seconds, assuming the first one didn't complete
// This won't prevent multiple transcript generated for the same room in a multi-instance deployment since state is not shared, but we're ok with the drawbacks
const LockMap = new ExpiryMap<string, boolean>(15000);
const serviceName = 'omnichannel-transcript' as const;
export const requestPdfTranscript = async (
room: AtLeast<IOmnichannelRoom, '_id' | 'open' | 'v' | 'pdfTranscriptFileId'>,
requestedBy: string,
): Promise<void> => {
if (room.open) {
throw new Error('room-still-open');
}
if (!room.v) {
throw new Error('improper-room-state');
}
// Don't request a transcript if there's already one requested
if (LockMap.has(room._id) || room.pdfTranscriptFileId) {
// TODO: use logger
logger.info({ msg: `Transcript already requested`, roomId: room._id });
return;
}
LockMap.set(room._id, true);
const details = { details: { rid: room._id, userId: requestedBy, from: serviceName } };
// Make the whole process sync when running on test mode
// This will prevent the usage of timeouts on the tests of this functionality :)View on GitHub (pinned to b2c16d5842)