RocketChat/Rocket.Chat · error · Meteor.Error
error-invalid-room
error-invalid-room
Error message
Invalid room
What it means
Thrown by the exported helper getUsersOfRoomWithoutKeyMethod when canAccessRoomIdAsync(rid, userId) returns false — the room id does not exist or the user cannot access it (not a member, removed, or lacking view access). The guard runs before room members' E2E public keys are fetched, so membership information is never disclosed to unauthorized callers.
Source
Thrown at apps/meteor/server/meteor-methods/platform/getUsersOfRoomWithoutKey.ts:21
import { Subscriptions, Users } from '@rocket.chat/models';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { canAccessRoomIdAsync } from '../../lib/authorization/canAccessRoom';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
'e2e.getUsersOfRoomWithoutKey'(rid: IRoom['_id']): { users: Pick<IUser, '_id' | 'e2e'>[] };
}
}
export const getUsersOfRoomWithoutKeyMethod = async (
userId: string,
rid: IRoom['_id'],
): Promise<{ users: Pick<IUser, '_id' | 'e2e'>[] }> => {
if (!(await canAccessRoomIdAsync(rid, userId))) {
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.getUsersOfRoomWithoutKey' });
}
const subscriptions = await Subscriptions.findByRidWithoutE2EKey(rid, {
projection: { 'u._id': 1 },
}).toArray();
const userIds = subscriptions.map((s) => s.u._id);
const options = { projection: { 'e2e.public_key': 1 } };
const users = await Users.findByIdsWithPublicE2EKey(userIds, options).toArray();
return {
users,
};
};
Meteor.methods<ServerMethods>({
async 'e2e.getUsersOfRoomWithoutKey'(rid) {
check(rid, String);View on GitHub (pinned to b2c16d5842)
Solutions
- Verify the caller has a live subscription to rid before calling
- Re-resolve rid from the user's current subscriptions instead of cached state
- If membership was revoked, cancel the E2E key exchange flow for that room
- On error, clear local E2E room-key state instead of retrying
Example fix
// before
const { users } = await Meteor.callAsync('e2e.getUsersOfRoomWithoutKey', rid);
// after
if (!rid || !hasSubscriptionFor(rid)) {
// user is not (or no longer) in this room; drop stale E2E state
return dropLocalRoomKey(rid);
}
const { users } = await Meteor.callAsync('e2e.getUsersOfRoomWithoutKey', rid); Defensive patterns
Strategy: validation
Validate before calling
// client: confirm the caller belongs to the room before asking for member keys
// (miningo subscription cache on the client)
const subscription = chatSubscriptionCollection.findOne({ rid });
if (!subscription) {
// no membership (or room deleted): skip the method call
} Type guard
const isInvalidRoomError = (err: unknown): err is Meteor.Error => err instanceof Meteor.Error && err.error === 'error-invalid-room';
Try / catch
try {
const { users } = await Meteor.callAsync('e2e.getUsersOfRoomWithoutKey', rid);
} catch (err) {
if (isInvalidRoomError(err)) {
// drop stale local E2E key state for this room; do not retry
} else {
throw err;
}
} Prevention
- Derive rid from the user's live subscription rather than cached state
- Abort E2E key handshakes when room membership is lost
- Treat this error as a signal to clean up local room-key state, not to retry
When it happens
Trigger: Calling 'e2e.getUsersOfRoomWithoutKey' with a rid that was deleted; the caller was removed from the encrypted room mid key-handshake; the caller never joined the room and attempts to enumerate member public keys.
Common situations: Clients holding a stale cached rid after room deletion; E2E key exchange continuing after the user was kicked; probing private rooms with guessed ids.
Related errors
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/60d00bd461ba57f9.
Report an issue: GitHub.