{"record":{"id":"21a6aacc1620292d","repo":"RocketChat/Rocket.Chat","slug":"error-user-already-in-role","errorCode":"error-user-already-in-role","errorMessage":"User already in role","messagePattern":"User already in role","errorType":"exception","errorClass":"Meteor.Error","httpStatus":null,"severity":"warning","filePath":"apps/meteor/server/api/v1/roles.ts","lineNumber":144,"sourceCode":"\t\t\t\t400: validateBadRequestErrorResponse,\n\t\t\t\t401: validateUnauthorizedErrorResponse,\n\t\t\t},\n\t\t},\n\t\tasync function action() {\n\t\t\tconst user = await getUserFromParams(this.bodyParams);\n\t\t\tconst { roleId, roomId } = this.bodyParams;\n\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 role = await Roles.findOneById(roleId);\n\t\t\tif (!role) {\n\t\t\t\treturn API.v1.failure('error-role-not-found', 'Role not found');\n\t\t\t}\n\n\t\t\tif (await hasRoleAsync(user._id, role._id, roomId)) {\n\t\t\t\tthrow new Meteor.Error('error-user-already-in-role', 'User already in role');\n\t\t\t}\n\n\t\t\tawait addUserToRole(this.userId, role._id, user.username, roomId);\n\n\t\t\treturn API.v1.success({\n\t\t\t\trole,\n\t\t\t});\n\t\t},\n\t)\n\t.get(\n\t\t'roles.getUsersInRole',\n\t\t{\n\t\t\tauthRequired: true,\n\t\t\tpermissionsRequired: ['access-permissions'],\n\t\t\tquery: isRolesGetUsersInRoleProps,\n\t\t\tresponse: {\n\t\t\t\t200: ajv.compile<{ users: IUserInRole[]; total: number }>({\n\t\t\t\t\ttype: 'object',","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/f9d3ec372bb580fa8d036f94cf03925a478ef768/apps/meteor/server/api/v1/roles.ts#L126-L162","documentation":"Thrown by POST roles.addUserToRole when hasRoleAsync(user._id, role._id, roomId) is already true. The role lookup succeeded and the user was resolved via getUserFromParams, but the user already holds the role (optionally scoped to roomId). Returns a structured Meteor.Error. Note earlier failures in this action use return API.v1.failure(...) instead of throwing.","triggerScenarios":"POST /api/v1/roles.addUserToRole with a user+role (+optional roomId scope) combination that the user is already a member of; re-running an idempotent assignment.","commonSituations":"UI 'add to role' button clicked twice; automation re-applies role assignments; migration script re-granting existing roles.","solutions":["Treat the error as success for idempotent workflows (user is already in the desired state).","Check membership first with GET /api/v1/roles.getUsersInRole before attempting the add.","When scoping by roomId, verify whether the role is room-scoped before re-assigning."],"exampleFix":"// before\nawait fetch('/api/v1/roles.addUserToRole', { method:'POST', body: JSON.stringify({ roleId, username }) });\n\n// after - treat already-in-role as idempotent success\ntry {\n  await fetch('/api/v1/roles.addUserToRole', { method:'POST', body: JSON.stringify({ roleId, username }) });\n} catch (e) {\n  if (e.error === 'error-user-already-in-role') return; // already desired state\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await fetch('/api/v1/roles.addUserToRole', { method:'POST', body: JSON.stringify({ roleId, username }) });\n} catch (e) {\n  if (e.error === 'error-user-already-in-role') return; // idempotent success\n  throw e;\n}","preventionTips":["Treat already-in-role as the desired end state in automation.","For non-idempotent UIs, check membership first via roles.getUsersInRole."],"tags":["roles","rest-api","conflict","idempotent","authorization"],"backgroundTag":null,"analyzedSha":"f9d3ec372bb580fa8d036f94cf03925a478ef768","analyzedAt":"2026-08-12T19:07:17.372Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}