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
- Fetch the user with GET /api/v1/users.info (fields: roles) and skip the call when the user lacks the role in that scope
- Match the scope to how the role was granted: omit scope for global roles, pass the room id for room-scoped grants
- Treat the error as success in idempotent retry logic when the goal is simply 'user must not have this role'
- 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
- Match the scope to how the grant was made (global vs room-scoped)
- Make removal flows idempotent: absence of the role is success
- Re-read user roles right before mutating them in concurrent-admin environments
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
- error-user-already-in-role
- error-admin-required
- error-invalid-param
- error-invalid-role
- error-invalid-roleId
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)