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

  1. Await the room-closing flow (closeRoom / afterClose callbacks) before requesting the transcript.
  2. Re-fetch the room and confirm open === false immediately before calling; treat 'room-still-open' as 'defer', not 'fail'.
  3. 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

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


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)