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
- Load the room including v in the projection before calling (mirror the official route: project _id, open, v, t, pdfTranscriptFileId).
- Pre-validate room.v and fail with your own descriptive error before invoking the helper.
- 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
- Project v (and open, t, pdfTranscriptFileId) whenever a room object will feed requestPdfTranscript.
- Avoid passing synthetic/partial room objects between helpers; re-fetch full documents at boundaries.
- Treat improper-room-state as a data-integrity signal, not a transient failure - never blind-retry it.
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
- room-still-open
- 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/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)