RocketChat/Rocket.Chat · error · Error
User not subscribed to room
Error message
User not subscribed to room
What it means
Thrown by getUnreadByUser when Subscriptions.findOneByRoomIdAndUserId returns no document for the given room/user pair. In Rocket.Chat a user must have an active subscription (membership) to a room before message-read state is tracked, so the bridge refuses to compute unread messages for a non-member.
Source
Thrown at apps/meteor/app/apps/server/bridges/rooms.ts:330
const users = await Users.findByIds(subs.map((user: { uid: string }) => user.uid)).toArray();
const userConverter = this.orch.getConverters().get('users');
return users.map((user: ICoreUser) => userConverter.convertToApp(user));
}
protected async getUnreadByUser(roomId: string, uid: string, options: GetMessagesOptions, appId: string): Promise<Array<IMessageRaw>> {
this.orch.debugLog(`The App ${appId} is getting the unread messages for the user: "${uid}" in the room: "${roomId}"`);
const messageConverter = this.orch.getConverters()?.get('messages');
if (!messageConverter) {
throw new Error('Message converter not found');
}
const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, uid, { projection: { ls: 1 } });
if (!subscription) {
const errorMessage = `No subscription found for user with ID "${uid}" in room with ID "${roomId}". This means the user is not subscribed to the room.`;
this.orch.debugLog(errorMessage);
throw new Error('User not subscribed to room');
}
const lastSeen = subscription?.ls;
if (!lastSeen) {
return [];
}
const sort: Sort = options.sort?.createdAt ? { ts: options.sort.createdAt } : { ts: 1 };
const cursor = Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes(
roomId,
lastSeen,
new Date(),
[],
{
limit: options.limit,
skip: options.skip,
sort,View on GitHub (pinned to f9d3ec372b)
Solutions
- Filter the user list to actual room members before calling getUnreadByUser (use the room's membership readers).
- Wrap the call per-user in try/catch and skip users who are not subscribed.
- Re-fetch membership right before the call if the room's roster can change concurrently.
Example fix
// before
for (const uid of allUserIds) {
const unread = await read.getRoomReader().getUnreadByUser(roomId, uid);
}
// after
for (const uid of allUserIds) {
try {
const unread = await read.getRoomReader().getUnreadByUser(roomId, uid);
} catch (err) {
// user is not a member of the room; skip
continue;
}
} Defensive patterns
Strategy: validation
Validate before calling
const members = await read.getRoomReader().getMembers(roomId); const memberIds = new Set(members.map((m) => m.id)); const safeUserIds = allUserIds.filter((uid) => memberIds.has(uid));
Try / catch
for (const uid of allUserIds) {
try {
const unread = await read.getRoomReader().getUnreadByUser(roomId, uid);
} catch (err) {
if (err instanceof Error && err.message === 'User not subscribed to room') {
continue;
}
throw err;
}
} Prevention
- Derive the user list from the room's actual subscriptions.
- Skip non-members rather than crashing a batch operation.
- Re-check membership if the roster can change between gather and use.
When it happens
Trigger: An app calls getUnreadByUser(roomId, uid) for a user who has never joined the room, was removed from it, or whose subscription was deleted. The debug log records the exact room and user ids.
Common situations: App iterates a list of user ids that includes non-members; user left or was kicked between the app gathering the list and reading unread counts; livechat visitors or bot users queried against a room they are not subscribed to.
Related errors
- There must be a parent room to create a discussion.
- roomId was not provided.
- Invalid Api parameter provided, it must be a valid IApi obje
- Invalid command parameter provided, must be a string.
- Invalid Slash Command parameter provided, it must be a valid
AI-assisted analysis of RocketChat/Rocket.Chat@f9d3ec372b (2026-08-12).
Data as JSON: /api/errors/46a84d23683fc513.
Report an issue: GitHub.