{"record":{"id":"56a9640a71a66978","repo":"RocketChat/Rocket.Chat","slug":"error-room-already-closed-56a964","errorCode":"error-room-already-closed","errorMessage":"error-room-already-closed","messagePattern":"error-room-already-closed","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"apps/meteor/server/lib/omnichannel/closeLivechatRoom.ts","lineNumber":78,"sourceCode":"\t\t\t\t\t\temailTranscript: {\n\t\t\t\t\t\t\tsendToVisitor: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t}),\n\t};\n\n\tif (forceClose) {\n\t\treturn closeRoom({\n\t\t\troom,\n\t\t\tuser,\n\t\t\toptions,\n\t\t\tcomment,\n\t\t\tforceClose,\n\t\t});\n\t}\n\n\tif (!room.open) {\n\t\tthrow new Error('error-room-already-closed');\n\t}\n\n\treturn closeRoom({\n\t\troom,\n\t\tuser,\n\t\toptions,\n\t\tcomment,\n\t});\n};\n","sourceCodeStart":60,"sourceCodeEnd":88,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/lib/omnichannel/closeLivechatRoom.ts#L60-L88","documentation":"The outer closeLivechatRoom wrapper throws Error('error-room-already-closed') when the loaded room has open=false and forceClose was not requested. Its purpose is to stop double-closing an omnichannel room; passing forceClose: true bypasses it and re-runs the close routine.","triggerScenarios":"Calling closeLivechatRoom twice for the same rid - double click, client retry after a timeout, webhook re-delivery - without forceClose; or closing a room already closed by the other agent, a routing rule, or an inactivity auto-close.","commonSituations":"Retry logic without idempotency awareness; UI room state not refreshed after the first successful close; races between manual close and automatic closure (inactivity timeout, business hours end).","solutions":["Treat the error as success in idempotent close flows - the desired end state is already reached","Guard the close control against double submission and refresh room state after closing succeeds","Pass forceClose: true only when re-closing an already-closed room is genuinely intended (e.g. correcting closing metadata)"],"exampleFix":"// before - naive close, throws on the second call\nawait closeLivechatRoom(rid, user, { clientAction: true });\n\n// after - idempotent close\ntry {\n  await closeLivechatRoom(rid, user, { clientAction: true });\n} catch (err: any) {\n  if (err?.message !== 'error-room-already-closed') throw err;\n  // already closed - nothing left to do\n}","handlingStrategy":"type-guard","validationCode":"const room = await LivechatRooms.findOneById(roomId);\nif (!room?.open) {\n  // nothing to close: return success (idempotent) or inform the caller the room is closed\n}","typeGuard":"import { isOmnichannelRoom } from '@rocket.chat/core-typings';\n\nconst isCloseableOmnichannelRoom = (room: any, forceClose = false): room is IOmnichannelRoom =>\n  !!room && isOmnichannelRoom(room) && (forceClose || room.open === true);","tryCatchPattern":"try {\n  await closeLivechatRoom(roomId, user, { clientAction: true });\n} catch (err: any) {\n  if (err?.message === 'error-room-already-closed') return; // goal state already reached\n  throw err;\n}","preventionTips":["Disable the close control once room.open flips false; refresh room state after each close","Make API/webhook close handlers idempotent by swallowing 'already closed'","Reserve forceClose for deliberate re-close operations (e.g. correcting closing metadata)"],"tags":["omnichannel","livechat","room","state","idempotency"],"backgroundTag":"invalid-room-state","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}