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

  1. Verify the caller has a live subscription to rid before calling
  2. Re-resolve rid from the user's current subscriptions instead of cached state
  3. If membership was revoked, cancel the E2E key exchange flow for that room
  4. 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

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.