{"record":{"id":"faaa4d9b5ec2434c","repo":"RocketChat/Rocket.Chat","slug":"room-still-open","errorCode":null,"errorMessage":"room-still-open","messagePattern":"room-still-open","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/ee/server/lib/omnichannel/requestPdfTranscript.ts","lineNumber":17,"sourceCode":"import { OmnichannelTranscript, QueueWorker } from '@rocket.chat/core-services';\nimport type { AtLeast, IOmnichannelRoom } from '@rocket.chat/core-typings';\nimport 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 :)","sourceCodeStart":1,"sourceCodeEnd":35,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/ee/server/lib/omnichannel/requestPdfTranscript.ts#L1-L35","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Await the room-closing flow (closeRoom / afterClose callbacks) before requesting the transcript.","Re-fetch the room and confirm open === false immediately before calling; treat 'room-still-open' as 'defer', not 'fail'.","In the UI, disable the request-transcript action while the room is open so users cannot trigger it early."],"exampleFix":"// before\nawait closeRoom(rid, userId); // queued elsewhere, not awaited\nawait requestPdfTranscript(room, userId); // room.open still true -> throws\n\n// after\nconst room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1, v: 1, t: 1, pdfTranscriptFileId: 1 } });\nif (!room || room.open) throw new Error('room-still-open');\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 || room.open) {\n\t// room must be closed before a transcript can be requested; defer or reject here\n}","typeGuard":null,"tryCatchPattern":"try {\n\tawait requestPdfTranscript(room, userId);\n} catch (e: any) {\n\tif (e?.message === 'room-still-open') { scheduleRetryAfterRoomCloses(rid); return; }\n\tthrow e;\n}","preventionTips":["Sequence operations: await the close flow before requesting the transcript.","Disable transcript-request UI actions while the room is open.","Always load the room fresh (with open projected) right before the call rather than reusing a cached object."],"tags":["omnichannel","pdf-transcript","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"}