{"record":{"id":"007601187fe2624c","repo":"RocketChat/Rocket.Chat","slug":"error-invalid-subscription","errorCode":"error-invalid-subscription","errorMessage":"Invalid subscription","messagePattern":"Invalid subscription","errorType":"exception","errorClass":"Meteor.Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/server/api/v1/rooms.ts","lineNumber":463,"sourceCode":"\t{\n\t\tauthRequired: true,\n\t\tbody: saveDraftBodySchema,\n\t\tresponse: {\n\t\t\t200: saveDraftResponseSchema,\n\t\t\t400: validateBadRequestErrorResponse,\n\t\t\t401: validateUnauthorizedErrorResponse,\n\t\t},\n\t},\n\tasync function action() {\n\t\tconst { rid, draft, tmid } = this.bodyParams;\n\n\t\tif (draft.length > (settings.get<number>('Message_MaxAllowedSize') ?? 0)) {\n\t\t\treturn API.v1.failure('error-message-size-exceeded');\n\t\t}\n\n\t\tconst subscription = await Subscriptions.updateDraftByRoomIdAndUserId(rid, this.userId, draft || undefined, tmid);\n\t\tif (!subscription) {\n\t\t\tthrow new Meteor.Error('error-invalid-subscription', 'Invalid subscription');\n\t\t}\n\n\t\tvoid notifyOnSubscriptionChanged(subscription);\n\n\t\treturn API.v1.success();\n\t},\n);\n\nAPI.v1.post(\n\t'rooms.cleanHistory',\n\t{\n\t\tauthRequired: true,\n\t\tbody: isRoomsCleanHistoryProps,\n\t\tresponse: {\n\t\t\t200: ajv.compile<{ _id: string; count: number }>({\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\t_id: { type: 'string' },","sourceCodeStart":445,"sourceCodeEnd":481,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/f9d3ec372bb580fa8d036f94cf03925a478ef768/apps/meteor/server/api/v1/rooms.ts#L445-L481","documentation":"Thrown by POST rooms.saveDraft when Subscriptions.updateDraftByRoomIdAndUserId matched zero rows — i.e. the calling user has no subscription (membership) for that room. Drafts are stored on the membership record, so no membership means no draft can be saved.","triggerScenarios":"POST /api/v1/rooms.saveDraft with { rid, draft, tmid } for a room the authenticated user is not a member of, has left, or was removed from.","commonSituations":"User left the channel but the client still has it open; bots calling on behalf of users who aren't members; dry-run/test accounts; race after a kick/ban.","solutions":["Verify membership before saving the draft (subscriptions.getOne?roomId=).","If the user has left, suppress draft-save calls from the client for that room.","For bots, ensure the bot user is added to the room before invoking saveDraft."],"exampleFix":"// before\nawait rest.post('/api/v1/rooms.saveDraft', { rid, draft });\n\n// after\nconst sub = await rest.get(`/api/v1/subscriptions.getOne?roomId=${rid}`);\nif (sub.subscription) {\n  await rest.post('/api/v1/rooms.saveDraft', { rid, draft });\n}","handlingStrategy":"validation","validationCode":"const { subscription } = await rest.get(`/api/v1/subscriptions.getOne?roomId=${rid}`);\nif (!subscription) {\n  // user isn't a member — skip draft save silently\n  return;\n}\nawait rest.post('/api/v1/rooms.saveDraft', { rid, draft, tmid });","typeGuard":"function isRoomMember(sub: unknown): boolean {\n  return !!sub && typeof (sub as any).rid === 'string';\n}","tryCatchPattern":"try {\n  await rest.post('/api/v1/rooms.saveDraft', { rid, draft, tmid });\n} catch (e) {\n  if (isMeteorError(e, 'error-invalid-subscription')) {\n    // leave the draft local; user is not a member\n  } else throw e;\n}","preventionTips":["Drop draft-save timers for rooms the user has left.","For bots, ensure room membership before calling.","Treat this error as non-fatal — keep the draft client-side."],"tags":["subscriptions","drafts","membership","rest-api"],"backgroundTag":null,"analyzedSha":"f9d3ec372bb580fa8d036f94cf03925a478ef768","analyzedAt":"2026-08-12T19:07:17.372Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}