RocketChat/Rocket.Chat · error · Meteor.Error

error-user-not-in-role

error-user-not-in-role

Error message

User is not in this role

What it means

Thrown by POST roles.removeUserFromRole when both the user and the role exist but hasAnyRoleAsync(user._id, [role._id], scope) returns false: the user does not hold that role in the requested scope. Scope is optional; omitting it checks the user's global roles, while passing a room id restricts the check to that room's role grants. A scope mismatch (role held globally, removal scoped to a room, or the reverse) therefore fails.

Solutions

  1. Fetch the user with GET /api/v1/users.info (fields: roles) and skip the call when the user lacks the role in that scope
  2. Match the scope to how the role was granted: omit scope for global roles, pass the room id for room-scoped grants
  3. Treat the error as success in idempotent retry logic when the goal is simply 'user must not have this role'
  4. Refresh your membership cache after concurrent admins change roles

Example fix

// before
await sdk.post('roles.removeUserFromRole', { roleId, username }); // retry after timeout, already removed

// after
const { user } = await sdk.get('users.info', { username, fields: JSON.stringify({ roles: 1 }) });
if (!scope && !user.roles?.includes(roleId)) return; // already absent -> no-op
await sdk.post('roles.removeUserFromRole', { roleId, username, scope });
Defensive patterns

Strategy: validation

Validate before calling

const { user } = await sdk.get('users.info', { username, fields: JSON.stringify({ roles: 1 }) });
if (!scope && !user.roles?.includes(roleId)) return; // already absent -> skip, avoids error-user-not-in-role

Try / catch

try {
  await sdk.post('roles.removeUserFromRole', { roleId, username, scope });
} catch (e: any) {
  if (e?.response?.data?.errorType === 'error-user-not-in-role') return; // goal already met
  throw e;
}

Prevention

When it happens

Trigger: Removing a role the user never had; removing a global role while passing scope=<roomId> (or removing a room-scoped grant without the matching scope); retrying a removal that already succeeded; two admins concurrently removing the same assignment where the second call loses.

Common situations: Idempotent-looking retry loops after a timeout where the first request actually succeeded; UI state showing stale role membership; scripts that always pass a room scope when the grant was global; test fixtures that assume a role was granted.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/fb34557669bf61b0. Report an issue: GitHub.

Appendix: source

Thrown at apps/meteor/server/api/v1/roles.ts:297

			if (!roleId) {
				return API.v1.failure('error-invalid-role-properties');
			}

			const user = await Users.findOneByUsername(username);

			if (!user) {
				throw new Meteor.Error('error-invalid-user', 'There is no user with this username');
			}

			const role = await Roles.findOneById(roleId);

			if (!role) {
				throw new Meteor.Error('error-invalid-roleId', 'This role does not exist');
			}

			if (!(await hasAnyRoleAsync(user._id, [role._id], scope))) {
				throw new Meteor.Error('error-user-not-in-role', 'User is not in this role');
			}

			if (role._id === 'admin') {
				const adminCount = await Roles.countUsersInRole('admin');
				if (adminCount === 1) {
					throw new Meteor.Error('error-admin-required', 'You need to have at least one admin');
				}
			}

			await removeUserFromRolesAsync(user._id, [role._id], scope);

			if (settings.get('UI_DisplayRoles')) {
				void api.broadcast('user.roleUpdate', {
					type: 'removed',
					_id: role._id,
					u: {
						_id: user._id,
						username: user.username,

View on GitHub (pinned to b2c16d5842)