{"record":{"id":"31d6dd3d6bee4e41","repo":"RocketChat/Rocket.Chat","slug":"error-extension-not-available","errorCode":"error-extension-not-available","errorMessage":"Extension is already assigned to another user","messagePattern":"Extension is already assigned to another user","errorType":"exception","errorClass":"MeteorError","httpStatus":null,"severity":"error","filePath":"apps/meteor/server/lib/users/saveUser/validateUserEditing.ts","lineNumber":21,"sourceCode":"import type { IUser } from '@rocket.chat/core-typings';\nimport { Users } from '@rocket.chat/models';\n\nimport type { UpdateUserData } from './saveUser';\nimport { settings } from '../../../settings';\nimport { hasPermissionAsync } from '../../authorization/hasPermission';\n\nconst isEditingUserRoles = (previousRoles: IUser['roles'], newRoles?: IUser['roles']) =>\n\tnewRoles !== undefined &&\n\t(newRoles.some((item) => !previousRoles.includes(item)) || previousRoles.some((item) => !newRoles.includes(item)));\nconst isEditingField = (previousValue?: string, newValue?: string) => typeof newValue !== 'undefined' && newValue !== previousValue;\n\nexport const canEditExtension = async (newExtension?: string) => {\n\tif (!settings.get('VoIP_TeamCollab_SIP_Integration_Enabled')) {\n\t\treturn false;\n\t}\n\n\tif (newExtension && (await Users.findOneByFreeSwitchExtension(newExtension, { projection: { _id: 1 } }))) {\n\t\tthrow new MeteorError('error-extension-not-available', 'Extension is already assigned to another user');\n\t}\n\n\treturn true;\n};\n\n/**\n * Validate permissions to edit user fields\n *\n * @param {string} userId\n * @param {{ _id: string, roles?: string[], username?: string, name?: string, statusText?: string, email?: string, password?: string}} userData\n */\nexport async function validateUserEditing(userId: IUser['_id'], userData: UpdateUserData): Promise<void> {\n\tconst editingMyself = userData._id && userId === userData._id;\n\n\tconst canEditOtherUserInfo = await hasPermissionAsync(userId, 'edit-other-user-info');\n\tconst canEditOtherUserPassword = await hasPermissionAsync(userId, 'edit-other-user-password');\n\tconst user = await Users.findOneById(userData._id);\n","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/lib/users/saveUser/validateUserEditing.ts#L3-L39","documentation":"Thrown by canEditExtension() (validateUserEditing.ts:21) when the VoIP/FreeSWITCH integration is enabled (VoIP_TeamCollab_SIP_Integration_Enabled), a new freeSwitchExtension value is being set, and Users.findOneByFreeSwitchExtension finds ANOTHER user already owning that extension number. Extensions are a one-user-per-number resource. Note the inverse case: when the VoIP setting is off, canEditExtension returns false and the caller instead gets the separate 'Edit user voice call extension is not allowed' error.","triggerScenarios":"users.update assigning freeSwitchExtension '1001' when a different user already has extension '1001' in the FreeSWITCH admin panel; reassigning an extension that a departed employee still holds; importing PBX users whose extensions overlap existing accounts.","commonSituations":"Admin console user form with the Voice Call extension field; VoIP setup where the PBX was renumbered but Rocket.Chat users kept old extensions; two admins editing simultaneously, the second one colliding.","solutions":["Find the current owner and release the number first: update that user's freeSwitchExtension to another value or clear it, then retry.","Assign a free extension: query users and exclude all freeSwitchExtension values already taken before choosing.","If the number genuinely belongs to the new user, deactivate or renumber the old holder as part of the same change."],"exampleFix":"// before\nawait POST '/api/v1/users.update', { userId: newId, data: { freeSwitchExtension: '1001' } }); // throws: in use\n\n// after: free it from the previous owner first\nawait POST '/api/v1/users.update', { userId: previousOwnerId, data: { freeSwitchExtension: '' } });\nawait POST '/api/v1/users.update', { userId: newId, data: { freeSwitchExtension: '1001' } });","handlingStrategy":"try-catch","validationCode":"// best-effort pre-check: extensions are not indexed in a public filter, so detect collisions via admin search\nconst taken = new Set((await GET '/api/v1/users.list?count=0&fields={\"freeSwitchExtension\":1}') /* page through */).users.map((u) => u.freeSwitchExtension));\nif (taken.has(ext)) throw new Error('extension in use');","typeGuard":"null","tryCatchPattern":"catch (e) {\n  if (e.error === 'error-extension-not-available') {\n    // locate owner via admin users list, release or pick another extension, then retry once\n  }\n}","preventionTips":["Maintain the extension directory outside Rocket.Chat as the source of truth.","Assign extensions from a pool that excludes used numbers.","Recheck on every VoIP renumbering project."],"tags":["users","voip","freeswitch","conflict","save-user"],"backgroundTag":"duplicate-resource-conflict","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}