{"record":{"id":"90225b03422df4c0","repo":"RocketChat/Rocket.Chat","slug":"error-app-prevented-90225b","errorCode":"error-app-prevented","errorMessage":"error.message","messagePattern":"error\\.message","errorType":"exception","errorClass":"Meteor.Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/server/meteor-methods/rooms/removeUserFromRoom.ts","lineNumber":92,"sourceCode":"\t\t\t});\n\t\t}\n\t}\n\n\tif (await hasRoleAsync(removedUser._id, 'owner', room._id)) {\n\t\tconst numOwners = await Roles.countUsersInRole('owner', room._id);\n\n\t\tif (numOwners === 1) {\n\t\t\tthrow new Meteor.Error('error-you-are-last-owner', 'You are the last owner. Please set new owner before leaving the room.', {\n\t\t\t\tmethod: 'removeUserFromRoom',\n\t\t\t});\n\t\t}\n\t}\n\n\ttry {\n\t\tawait Apps.self?.triggerEvent(AppEvents.IPreRoomUserLeave, room, removedUser, fromUser);\n\t} catch (error: any) {\n\t\tif (error.name === AppsEngineException.name) {\n\t\t\tthrow new Meteor.Error('error-app-prevented', error.message);\n\t\t}\n\n\t\tthrow error;\n\t}\n\n\tawait callbacks.run('beforeRemoveFromRoom', { removedUser, userWhoRemoved: fromUser }, room);\n\n\tconst deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(data.rid, removedUser._id);\n\tif (deletedSubscription) {\n\t\tvoid notifyOnSubscriptionChanged(deletedSubscription, 'removed');\n\t}\n\n\tif (['c', 'p'].includes(room.t) === true) {\n\t\tawait removeUserFromRolesAsync(removedUser._id, ['moderator', 'owner'], data.rid);\n\t}\n\n\tawait Message.saveSystemMessage('ru', data.rid, removedUser.username || '', fromUser);\n","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/meteor-methods/rooms/removeUserFromRoom.ts#L74-L110","documentation":"Thrown by 'removeUserFromRoom' when an installed Rocket.Chat App intercepts the IPreRoomUserLeave lifecycle event and throws an AppsEngineException to veto the removal. Apps can run pre-hooks before any user is kicked from a channel/group; a veto aborts the whole operation before the subscription is deleted. The thrown Meteor.Error carries the app's own message (error.message), so the reason text comes from the app, not Rocket.Chat core.","triggerScenarios":"A governance/compliance app blocking removal of users it tracks; an app enforcing retention of team members; any app registered on AppEvents.IPreRoomUserLeave that calls preventDefault/throws for this room-user pair. Triggered by both the DDP method and the REST channels.kick/groups.kick endpoints that reuse this logic.","commonSituations":"Enterprise workspaces with custom apps managing membership sync; marketplace apps that pin membership (e.g. SAML/LDAP-synced teams); an app upgraded with stricter rules that now vetoes kicks it previously allowed; locally developed app throwing AppsEngineException unintentionally inside the handler.","solutions":["Read the error message: it is the app's explanation for the veto; identify which installed app listens to IPreRoomUserLeave (Apps admin screen, or check each app's source for the event).","Adjust or disable the app's rule (or update its configuration) so the kick is permitted.","Temporarily disable the app if the removal is operationally required, kick the user, then re-enable.","If you develop the app, only throw AppsEngineException when truly necessary and return a descriptive message."],"exampleFix":"// app handler causing it (apps-engine)\n// before\nthis.getLogger().log('keeping user');\nthrow new AppsEngineException('no reason given'); // cryptic veto\n\n// after\nthrow new AppsEngineException(`User ${user.username} is required in ${room.name} by SyncApp; remove mapping first.`);","handlingStrategy":"try-catch","validationCode":"// Discover apps that can veto room-user-leave before rolling out kick automation\nconst apps = await (await fetch('/api/v1/apps?marketplace=false', { headers: adminHeaders })).json();\nconst vetoCapable = apps.apps.filter((a) => a.status === 'manual_enabled' && implementsIPreRoomUserLeave(a));\n// check each app's source/changelog for AppEvents.IPreRoomUserLeave handlers","typeGuard":null,"tryCatchPattern":"try {\n  await Meteor.callAsync('removeUserFromRoom', { rid, username });\n} catch (err) {\n  if (err instanceof Meteor.Error && err.error === 'error-app-prevented') {\n    // err.message comes from the app; surface it to the operator, never retry blindly\n    throw new Error(`An app blocked this removal: ${err.message}`);\n  }\n  throw err;\n}","preventionTips":["Audit installed apps for IPreRoomUserLeave handlers before building membership automation.","Keep veto reasons actionable: app authors should include who/why in the exception message.","For emergency removals, disable the vetoing app first, kick, then re-enable.","Never auto-retry this error — the app will keep vetoing."],"tags":["rocket-chat","apps-engine","lifecycle-hook","room-management","app-veto"],"backgroundTag":"app-hook-veto","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}