RocketChat/Rocket.Chat · error · Error

room-still-open

Error message

room-still-open

What it means

Thrown by requestPdfTranscript in requestPdfTranscript.ts:17 when the supplied room is still open (room.open truthy). PDF transcripts are only generated for closed omnichannel rooms; requesting one on an open room is a contract violation. NOTE: plain `new Error('room-still-open')`.

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 f9d3ec372b)

Solutions

  1. Ensure the room is closed (room.open === false) before invoking requestPdfTranscript.
  2. If close is async, wait for the close callback / room-updated subscription before requesting.
  3. Match by message 'room-still-open' and retry once the close settles.

Example fix

// before
await requestPdfTranscript(room, requestedBy);

// after
if (room.open) {
  await closeRoom(room._id); // ensure settled
  room = await Rooms.findOneById(room._id);
}
await requestPdfTranscript(room, requestedBy);
Defensive patterns

Strategy: validation

Validate before calling

if (room.open) {
  // ensure the close has settled
  await closeRoom(room._id);
  room = await Rooms.findOneById(room._id);
}

Type guard

const isRoomClosed = (room: { open?: boolean }) => room.open === false;

Try / catch

try { await requestPdfTranscript(room, requestedBy); }
catch (e) {
  if (e instanceof Error && e.message === 'room-still-open') { /* close first */ return; }
  throw e;
}

Prevention

When it happens

Trigger: Calling requestPdfTranscript before the chat was closed (CloseRoom), or right after a close that has not yet flushed to the room record being passed in. Passing a freshly-constructed room object whose open field defaults true.

Common situations: Race between client closing the room and triggering transcript; webhook fired on a 'transcript requested' event for a still-open room; UI lets the user click 'get transcript' too early.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@f9d3ec372b (2026-08-12). Data as JSON: /api/errors/faaa4d9b5ec2434c. Report an issue: GitHub.