{"record":{"id":"c505ff113de2c920","repo":"RocketChat/Rocket.Chat","slug":"error-not-allowed-c505ff","errorCode":"error-not-allowed","errorMessage":"Not allowed","messagePattern":"Not allowed","errorType":"exception","errorClass":"Meteor.Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/server/meteor-methods/messages/getMessages.ts","lineNumber":29,"sourceCode":"\tinterface ServerMethods {\n\t\tgetMessages(messages: IMessage['_id'][]): Promise<IMessage[]>;\n\t}\n}\n\nMeteor.methods<ServerMethods>({\n\tasync getMessages(messages) {\n\t\tcheck(messages, [String]);\n\t\tconst uid = Meteor.userId();\n\n\t\tif (!uid) {\n\t\t\tthrow new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getMessages' });\n\t\t}\n\n\t\tconst msgs = await Messages.findVisibleByIds(messages).toArray();\n\t\tconst rids = await Promise.all([...new Set(msgs.map((m) => m.rid))].map((_id) => canAccessRoomIdAsync(_id, uid)));\n\n\t\tif (!rids.every(Boolean)) {\n\t\t\tthrow new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getSingleMessage' });\n\t\t}\n\n\t\treturn msgs;\n\t},\n});\n","sourceCodeStart":11,"sourceCodeEnd":35,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/meteor-methods/messages/getMessages.ts#L11-L35","documentation":"After fetching the requested messages, getMessages maps every distinct room through canAccessRoomIdAsync; if ANY room is inaccessible it throws 'error-not-allowed' (note: the thrown details say method 'getSingleMessage' — a copy-paste artifact in the source). One inaccessible message rejects the whole batch, including all accessible messages.","triggerScenarios":"Meteor.call('getMessages', ids) where at least one id belongs to a private room or DM the caller cannot access — e.g. quoted messages from channels the user was removed from, or ids harvested from search results or links.","commonSituations":"Resolving quoted-message previews across mixed rooms; users removed from channels retaining old references; ids aggregated from multiple sources into one request.","solutions":["Pre-filter ids: only request messages the client knows belong to rooms the user is subscribed to","Split the request into per-room or smaller batches so one bad id does not sink the rest","Catch 'error-not-allowed' and degrade to per-message lookups (getSingleMessage), skipping the inaccessible ones"],"exampleFix":"// before\nconst msgs = await Meteor.callAsync('getMessages', ids);\n\n// after — fall back to per-id fetch on batch rejection\ntry {\n  msgs = await Meteor.callAsync('getMessages', ids);\n} catch (e) {\n  if (e.error !== 'error-not-allowed') throw e;\n  msgs = (await Promise.allSettled(\n    ids.map((id) => Meteor.callAsync('getSingleMessage', id)),\n  )).filter((r) => r.status === 'fulfilled').map((r) => r.value);\n}","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"async function getMessagesSafe(ids: string[]) {\n  try {\n    return await Meteor.callAsync('getMessages', ids);\n  } catch (e) {\n    if ((e as Meteor.Error).error !== 'error-not-allowed') throw e;\n    // one bad room poisons the batch: degrade to per-id fetch, skip failures\n    const results = await Promise.allSettled(\n      ids.map((id) => Meteor.callAsync('getSingleMessage', id)),\n    );\n    return results\n      .filter((r): r is PromiseFulfilledResult<any> => r.status === 'fulfilled')\n      .map((r) => r.value)\n      .filter(Boolean);\n  }\n}","preventionTips":["Batch only ids whose rooms the user is known to subscribe to","Split heterogeneous id lists (search results, quotes) into per-room requests","Treat batch 'error-not-allowed' as partial data, not a total failure"],"tags":["meteor","authorization","rooms","batch-fetch"],"backgroundTag":"permission-denied","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}