RocketChat/Rocket.Chat · error · Meteor.Error

error-app-prevented-deleting

error-app-prevented-deleting

Error message

A Rocket.Chat App prevented the message deleting.

What it means

deleteMessage (deleteMessage.ts:38-44) fires the Apps Engine IPreMessageDeletePrevent event before deleting. If an installed app returns a truthy 'prevent' result, the server aborts with error-app-prevented-deleting 'A Rocket.Chat App prevented the message deleting.' This is deliberate app-side policy (e.g. retention/legal hold, moderation apps), not a bug.

Source

Thrown at apps/meteor/server/lib/messages/deleteMessage.ts:42

	if (!originalMessage || !user || !(await canDeleteMessageAsync(user, originalMessage))) {
		throw new Meteor.Error('error-action-not-allowed', 'Not allowed');
	}

	return deleteMessage(originalMessage, user);
};

export async function deleteMessage(message: IMessage, user: IUser): Promise<void> {
	const deletedMsg: IMessage | null = await Messages.findOneById(message._id);
	const isThread = (deletedMsg?.tcount || 0) > 0;
	const keepHistory = settings.get('Message_KeepHistory') || isThread;
	const showDeletedStatus = settings.get('Message_ShowDeletedStatus') || isThread;

	const room = await Rooms.findOneById(message.rid, { projection: { lastMessage: 1, prid: 1, mid: 1, federated: 1, federation: 1 } });

	if (deletedMsg) {
		const prevent = await Apps.self?.triggerEvent(AppEvents.IPreMessageDeletePrevent, deletedMsg);
		if (prevent) {
			throw new Meteor.Error('error-app-prevented-deleting', 'A Rocket.Chat App prevented the message deleting.');
		}

		if (room) {
			await Message.beforeDelete(deletedMsg, room);
		}
	}

	if (deletedMsg && isThreadMessage(deletedMsg)) {
		await deleteThreadMessage(deletedMsg, user, room);
	}

	const files = (message.files || [message.file]).filter(Boolean); // Keep compatibility with old messages

	if (keepHistory) {
		if (showDeletedStatus) {
			// TODO is there a better way to tell TS "IUser[username]" is not undefined?
			await Messages.cloneAndSaveAsHistoryById(message._id, user as Required<Pick<IUser, '_id' | 'username' | 'name'>>);
		} else {

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Check Administration > Apps for apps registering IPreMessageDeletePrevent and read their rules/logs to see why this message is protected
  2. Delete via the app's own escape hatch if it provides one (e.g. a moderator command), or remove/relax the app policy
  3. If the prevention is unintended, disable the app and retry the deletion
  4. If you author the app, only return true deliberately and log the reason alongside
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await deleteMessage(message, user);
} catch (error: any) {
  if (error instanceof Meteor.Error && error.error === 'error-app-prevented-deleting') {
    showAppProtectedMessage(); // an App holds this message; do not retry
    return;
  }
  throw error;
}

Prevention

When it happens

Trigger: An app implementing IPreMessageDeletePrevent (retention hold, audit, anti-abuse) returning true for the message being deleted; deleting messages inside a room covered by such an app's rules; app regression causing it to prevent all deletions.

Common situations: Compliance installs with retention apps holding messages; moderation apps blocking deletes of flagged content; admins unaware an installed app intercepts deletes; app authors accidentally returning truthy values from the hook.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/b11f7973f6647c9d. Report an issue: GitHub.