{"record":{"id":"f2d759427897f643","repo":"RocketChat/Rocket.Chat","slug":"error-invalid-subscription-f2d759","errorCode":"error-invalid-subscription","errorMessage":"error-invalid-subscription","messagePattern":"error-invalid-subscription","errorType":"exception","errorClass":"Error","httpStatus":400,"severity":"error","filePath":"apps/meteor/server/api/v1/subscriptions.ts","lineNumber":147,"sourceCode":"\nAPI.v1.post(\n\t'subscriptions.read',\n\t{\n\t\tauthRequired: true,\n\t\tbody: isSubscriptionsReadProps,\n\t\tresponse: {\n\t\t\t200: voidSuccessResponseSchema,\n\t\t\t400: validateBadRequestErrorResponse,\n\t\t\t401: validateUnauthorizedErrorResponse,\n\t\t},\n\t},\n\tasync function action() {\n\t\tconst { readThreads = false } = this.bodyParams;\n\t\tconst roomId = 'rid' in this.bodyParams ? this.bodyParams.rid : this.bodyParams.roomId;\n\n\t\tconst room = await Rooms.findOneById(roomId);\n\t\tif (!room) {\n\t\t\tthrow new Error('error-invalid-subscription');\n\t\t}\n\n\t\tawait readMessages(room, this.userId, readThreads);\n\n\t\treturn API.v1.success();\n\t},\n);\n\nAPI.v1.post(\n\t'subscriptions.unread',\n\t{\n\t\tauthRequired: true,\n\t\tbody: isSubscriptionsUnreadProps,\n\t\tresponse: {\n\t\t\t200: voidSuccessResponseSchema,\n\t\t\t400: validateBadRequestErrorResponse,\n\t\t\t401: validateUnauthorizedErrorResponse,\n\t\t},","sourceCodeStart":129,"sourceCodeEnd":165,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/f9d3ec372bb580fa8d036f94cf03925a478ef768/apps/meteor/server/api/v1/subscriptions.ts#L129-L165","documentation":"BUG-WARNING: this is thrown as a plain `new Error('error-invalid-subscription')`, NOT `new Meteor.Error(...)`, so it will not produce the standard REST error envelope — clients may see a generic 500 instead of a structured error. Additionally the check is on the ROOM (Rooms.findOneById returned null) for POST subscriptions.read, but the code/message say 'subscription'. Semantically: the room id (rid/roomId) you passed does not exist.","triggerScenarios":"POST /api/v1/subscriptions.read with { rid } or { roomId } where no room matches that id. Despite the label, membership is not checked here — only room existence.","commonSituations":"Client holds a stale/deleted room id; room was removed between open and mark-read; typo in rid; bot acting on a room it never joined.","solutions":["Confirm the room exists (rooms.info) before calling subscriptions.read.","Drop stale room ids from the client's open-room list when rooms are deleted.","Upstream: change `new Error(...)` to `new Meteor.Error('error-invalid-room', ...)` to match the actual condition and produce a proper REST error."],"exampleFix":"// before\nawait rest.post('/api/v1/subscriptions.read', { rid });\n\n// after\nconst info = await rest.get(`/api/v1/rooms.info?roomId=${rid}`);\nif (!info.room) { /* room gone — drop it locally */ return; }\nawait rest.post('/api/v1/subscriptions.read', { rid });","handlingStrategy":"validation","validationCode":"const info = await rest.get(`/api/v1/rooms.info?roomId=${roomId}`);\nif (!info.room) {\n  // room does not exist; do not call subscriptions.read\n  return;\n}","typeGuard":"function roomExists(info: { room?: unknown }): boolean {\n  return !!info.room;\n}","tryCatchPattern":"try {\n  await rest.post('/api/v1/subscriptions.read', { rid: roomId });\n} catch (e) {\n  // NOTE: thrown as plain Error, not Meteor.Error — may surface as a 500\n  if (String(e.message ?? e).includes('error-invalid-subscription')) {\n    // room not found; drop from local state\n  } else throw e;\n}","preventionTips":["Confirm the room exists before marking read.","Drop deleted rooms from client state on push events.","Upstream fix: throw new Meteor.Error('error-invalid-room', ...) and check room, not subscription."],"tags":["subscriptions","rooms","rest-api","bug-plain-error","bug-mismatched-code"],"backgroundTag":null,"analyzedSha":"f9d3ec372bb580fa8d036f94cf03925a478ef768","analyzedAt":"2026-08-12T19:07:17.372Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}