RocketChat/Rocket.Chat · error · Error

improper-room-state

Error message

improper-room-state

What it means

After the closed check, requestPdfTranscript requires room.v - the livechat visitor object - to build the transcript, and throws Error('improper-room-state') when it is missing. The visitor carries the identity printed on the PDF, so a room without visitor data cannot be transcribed. In practice this means the room document was loaded with a projection that excludes v, or the stored room itself lacks visitor data.

Solutions

  1. Load the room including v in the projection before calling (mirror the official route: project _id, open, v, t, pdfTranscriptFileId).
  2. Pre-validate room.v and fail with your own descriptive error before invoking the helper.
  3. If visitor data is genuinely missing from the database, repair the room document or accept that no transcript can be generated for it.

Example fix

// before
const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1 } });
await requestPdfTranscript(room, userId); // throws improper-room-state: no v

// after
const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1, v: 1, t: 1, pdfTranscriptFileId: 1 } });
if (!room?.v) throw new Error('improper-room-state');
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?.v) {
	// visitor data missing: transcript cannot be generated; do not call requestPdfTranscript
}

Type guard

const isTranscriptableRoom = (
	room: unknown,
): room is AtLeast<IOmnichannelRoom, '_id' | 'open' | 'v' | 'pdfTranscriptFileId'> =>
	typeof room === 'object' && room !== null && !!(room as IOmnichannelRoom).v && !(room as IOmnichannelRoom).open;

Try / catch

try {
	await requestPdfTranscript(room, userId);
} catch (e: any) {
	if (e?.message === 'improper-room-state') { flagRoomDataIntegrity(rid); return; } // do not retry; data is incomplete
	throw e;
}

Prevention

When it happens

Trigger: Calling requestPdfTranscript with a room fetched using projections that omit v (the shipped REST route projects v: 1 precisely to avoid this); passing a synthetic/minimal room object that only has _id/open (the unit tests explicitly cover {} throwing improper-room-state); or a room whose visitor was deleted or never written.

Common situations: Custom integrations loading rooms with narrow projections and reusing the same object for transcript calls; test fixtures with partial room objects; data corruption after imports/migrations where the v field was dropped.

Related errors


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

Appendix: source

Thrown at apps/meteor/ee/server/lib/omnichannel/requestPdfTranscript.ts:21

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 :)
	if (process.env.TEST_MODE) {
		await OmnichannelTranscript.workOnPdf(details);
		return;
	}

View on GitHub (pinned to b2c16d5842)