{"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":400,"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/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/api/v1/roles.ts#L126-L162","documentation":"Thrown by POST /api/v1/roles.addUserToRole when hasRoleAsync(user._id, role._id, roomId) is already true — the target user already holds that role, globally or in the given room scope. The endpoint deliberately rejects double-assignment instead of being idempotent, so retries of a partially-succeeded flow or scripts that re-add existing roles hit this.","triggerScenarios":"POST roles.addUserToRole { username, roleId, roomId? } where the user already has the role: re-running an onboarding script; retrying after a network blip where the first attempt actually committed; adding a role in a room scope the user already holds there.","commonSituations":"Idempotency-unaware provisioning scripts (SCIM/LDAP sync) run twice; UI double-submit; migration re-applying role grants from a snapshot.","solutions":["Make callers idempotent: catch error-user-already-in-role and treat it as success","Pre-check with roles.getUsersInRole (needs access-permissions) or users.info before assigning","Fix the upstream retry logic if the same assignment is being pushed repeatedly"],"exampleFix":"// before\nawait sdk.post('roles.addUserToRole', { roleId, username });\n\n// after (idempotent assign)\ntry {\n  await sdk.post('roles.addUserToRole', { roleId, username });\n} catch (e) {\n  if (e.error !== 'error-user-already-in-role') throw e;\n  // already assigned — nothing to do\n}","handlingStrategy":"try-catch","validationCode":"// optional pre-check (global scope only — room-scoped grants are not in user.roles)\nconst { user } = await sdk.get('users.info', { username });\nif (!roomId && user.roles?.includes(roleName)) return { alreadyAssigned: true };","typeGuard":null,"tryCatchPattern":"wrap roles.addUserToRole in a catch that treats e.error === 'error-user-already-in-role' as success (idempotent assign); rethrow everything else.","preventionTips":["Write provisioning scripts idempotently from the start","Fix duplicate-trigger sources (double webhooks, retried syncs) rather than suppressing the error everywhere","Remember room-scoped assignments are checked per roomId"],"tags":["roles","users","duplicate","idempotency","rest-api"],"backgroundTag":"duplicate-assignment","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"}