{"record":{"id":"fb34557669bf61b0","repo":"RocketChat/Rocket.Chat","slug":"error-user-not-in-role","errorCode":"error-user-not-in-role","errorMessage":"User is not in this role","messagePattern":"User is not in this role","errorType":"exception","errorClass":"Meteor.Error","httpStatus":400,"severity":"error","filePath":"apps/meteor/server/api/v1/roles.ts","lineNumber":297,"sourceCode":"\n\t\t\tif (!roleId) {\n\t\t\t\treturn API.v1.failure('error-invalid-role-properties');\n\t\t\t}\n\n\t\t\tconst user = await Users.findOneByUsername(username);\n\n\t\t\tif (!user) {\n\t\t\t\tthrow new Meteor.Error('error-invalid-user', 'There is no user with this username');\n\t\t\t}\n\n\t\t\tconst role = await Roles.findOneById(roleId);\n\n\t\t\tif (!role) {\n\t\t\t\tthrow new Meteor.Error('error-invalid-roleId', 'This role does not exist');\n\t\t\t}\n\n\t\t\tif (!(await hasAnyRoleAsync(user._id, [role._id], scope))) {\n\t\t\t\tthrow new Meteor.Error('error-user-not-in-role', 'User is not in this role');\n\t\t\t}\n\n\t\t\tif (role._id === 'admin') {\n\t\t\t\tconst adminCount = await Roles.countUsersInRole('admin');\n\t\t\t\tif (adminCount === 1) {\n\t\t\t\t\tthrow new Meteor.Error('error-admin-required', 'You need to have at least one admin');\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait removeUserFromRolesAsync(user._id, [role._id], scope);\n\n\t\t\tif (settings.get('UI_DisplayRoles')) {\n\t\t\t\tvoid api.broadcast('user.roleUpdate', {\n\t\t\t\t\ttype: 'removed',\n\t\t\t\t\t_id: role._id,\n\t\t\t\t\tu: {\n\t\t\t\t\t\t_id: user._id,\n\t\t\t\t\t\tusername: user.username,","sourceCodeStart":279,"sourceCodeEnd":315,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/api/v1/roles.ts#L279-L315","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait sdk.post('roles.removeUserFromRole', { roleId, username }); // retry after timeout, already removed\n\n// after\nconst { user } = await sdk.get('users.info', { username, fields: JSON.stringify({ roles: 1 }) });\nif (!scope && !user.roles?.includes(roleId)) return; // already absent -> no-op\nawait sdk.post('roles.removeUserFromRole', { roleId, username, scope });","handlingStrategy":"validation","validationCode":"const { user } = await sdk.get('users.info', { username, fields: JSON.stringify({ roles: 1 }) });\nif (!scope && !user.roles?.includes(roleId)) return; // already absent -> skip, avoids error-user-not-in-role","typeGuard":null,"tryCatchPattern":"try {\n  await sdk.post('roles.removeUserFromRole', { roleId, username, scope });\n} catch (e: any) {\n  if (e?.response?.data?.errorType === 'error-user-not-in-role') return; // goal already met\n  throw e;\n}","preventionTips":["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"],"tags":["roles","idempotency","scope","rest-api"],"backgroundTag":"user-not-in-role","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","contentChangedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}