{"record":{"id":"da50c886824b43d8","repo":"RocketChat/Rocket.Chat","slug":"improper-room-state","errorCode":null,"errorMessage":"improper-room-state","messagePattern":"improper-room-state","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/ee/server/lib/omnichannel/requestPdfTranscript.ts","lineNumber":21,"sourceCode":"import ExpiryMap from 'expiry-map';\n\nimport { logger } from './logger';\n\n// Allow to request a transcript again after 15 seconds, assuming the first one didn't complete\n// 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\nconst LockMap = new ExpiryMap<string, boolean>(15000);\n\nconst serviceName = 'omnichannel-transcript' as const;\nexport const requestPdfTranscript = async (\n\troom: AtLeast<IOmnichannelRoom, '_id' | 'open' | 'v' | 'pdfTranscriptFileId'>,\n\trequestedBy: string,\n): Promise<void> => {\n\tif (room.open) {\n\t\tthrow new Error('room-still-open');\n\t}\n\n\tif (!room.v) {\n\t\tthrow new Error('improper-room-state');\n\t}\n\n\t// Don't request a transcript if there's already one requested\n\tif (LockMap.has(room._id) || room.pdfTranscriptFileId) {\n\t\t// TODO: use logger\n\t\tlogger.info({ msg: `Transcript already requested`, roomId: room._id });\n\t\treturn;\n\t}\n\n\tLockMap.set(room._id, true);\n\n\tconst details = { details: { rid: room._id, userId: requestedBy, from: serviceName } };\n\t// Make the whole process sync when running on test mode\n\t// This will prevent the usage of timeouts on the tests of this functionality :)\n\tif (process.env.TEST_MODE) {\n\t\tawait OmnichannelTranscript.workOnPdf(details);\n\t\treturn;\n\t}","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/ee/server/lib/omnichannel/requestPdfTranscript.ts#L3-L39","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1 } });\nawait requestPdfTranscript(room, userId); // throws improper-room-state: no v\n\n// after\nconst room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1, v: 1, t: 1, pdfTranscriptFileId: 1 } });\nif (!room?.v) throw new Error('improper-room-state');\nawait requestPdfTranscript(room, userId);","handlingStrategy":"validation","validationCode":"const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1, v: 1, t: 1, pdfTranscriptFileId: 1 } });\nif (!room?.v) {\n\t// visitor data missing: transcript cannot be generated; do not call requestPdfTranscript\n}","typeGuard":"const isTranscriptableRoom = (\n\troom: unknown,\n): room is AtLeast<IOmnichannelRoom, '_id' | 'open' | 'v' | 'pdfTranscriptFileId'> =>\n\ttypeof room === 'object' && room !== null && !!(room as IOmnichannelRoom).v && !(room as IOmnichannelRoom).open;","tryCatchPattern":"try {\n\tawait requestPdfTranscript(room, userId);\n} catch (e: any) {\n\tif (e?.message === 'improper-room-state') { flagRoomDataIntegrity(rid); return; } // do not retry; data is incomplete\n\tthrow e;\n}","preventionTips":["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."],"tags":["omnichannel","pdf-transcript","visitor","room-state","enterprise"],"backgroundTag":"invalid-room-state","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","contentChangedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}