Skip to content

fix(apps-engine): add rejection handlers to fire-and-forget post-event dispatches in AppListenerManager#39373

Open
smirk-dev wants to merge 1 commit intoRocketChat:developfrom
smirk-dev:fix/apps-engine-post-event-unhandled-promises
Open

fix(apps-engine): add rejection handlers to fire-and-forget post-event dispatches in AppListenerManager#39373
smirk-dev wants to merge 1 commit intoRocketChat:developfrom
smirk-dev:fix/apps-engine-post-event-unhandled-promises

Conversation

@smirk-dev
Copy link
Contributor

@smirk-dev smirk-dev commented Mar 5, 2026

Description

In AppListenerManager.executeListener(), post-event handlers are intentionally fire-and-forget — they must not block the caller waiting for app hooks to complete. However, 6 of these async methods were being called with neither await nor a catch() handler:

// before — dropped promise, no error visibility
case AppInterface.IPostMessageSent:
    this.executePostMessageSent(data as IMessage);
    return;

Root Cause

Calling an async function without await and without .catch() creates a dangling promise. If the async method rejects (e.g., an installed app throws inside its onPostMessageSent handler), Node.js emits an UnhandledPromiseRejection event. Since Node.js 15+, the default behavior is process termination on unhandled rejections.

Affected event cases

  • IPostMessageSent
  • IPostSystemMessageSent
  • IPostMessageDeleted
  • IPostMessageUpdated
  • IPostRoomCreate
  • IPostRoomDeleted
  • IPostExternalComponentOpened
  • IPostExternalComponentClosed

Fix

Uses void expr.catch(console.error) — makes the fire-and-forget intent explicit to TypeScript's no-floating-promises rule, while ensuring any rejection is surfaced in server logs rather than silently crashing the process:

// after — explicit fire-and-forget with error visibility
case AppInterface.IPostMessageSent:
    void this.executePostMessageSent(data as IMessage).catch(console.error);
    return;

All 208 existing tests pass.

Summary by CodeRabbit

Bug Fixes

  • Enhanced error handling in asynchronous event handlers to ensure errors are logged instead of being silently ignored, improving application reliability and debugging visibility.

…t dispatches

Post-event handlers (IPostMessageSent, IPostSystemMessageSent, IPostMessageDeleted,
IPostMessageUpdated, IPostRoomCreate, IPostRoomDeleted, IPostExternalComponentOpened,
IPostExternalComponentClosed) were async methods called without a .catch() handler.
If an app threw inside a post-event hook, the resulting unhandled promise rejection
could terminate the Node.js process (default behavior since Node 15+).

The fire-and-forget semantics are intentionally preserved — post-events must not
block the caller — but each call now uses void expr.catch(console.error) so that
errors are visible in server logs without crashing the process.
@smirk-dev smirk-dev requested a review from a team as a code owner March 5, 2026 14:48
@dionisio-bot
Copy link
Contributor

dionisio-bot bot commented Mar 5, 2026

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label
  • This PR is missing the required milestone or project

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot
Copy link

changeset-bot bot commented Mar 5, 2026

🦋 Changeset detected

Latest commit: b35c872

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 42 packages
Name Type
@rocket.chat/apps-engine Patch
@rocket.chat/meteor Patch
@rocket.chat/apps Patch
@rocket.chat/core-services Patch
@rocket.chat/core-typings Patch
@rocket.chat/fuselage-ui-kit Patch
@rocket.chat/rest-typings Patch
@rocket.chat/ddp-streamer Patch
@rocket.chat/presence Patch
rocketchat-services Patch
@rocket.chat/uikit-playground Patch
@rocket.chat/ui-voip Patch
@rocket.chat/api-client Patch
@rocket.chat/cron Patch
@rocket.chat/ddp-client Patch
@rocket.chat/gazzodown Patch
@rocket.chat/http-router Patch
@rocket.chat/livechat Patch
@rocket.chat/model-typings Patch
@rocket.chat/ui-avatar Patch
@rocket.chat/ui-client Patch
@rocket.chat/ui-contexts Patch
@rocket.chat/web-ui-registration Patch
@rocket.chat/account-service Patch
@rocket.chat/authorization-service Patch
@rocket.chat/omnichannel-transcript Patch
@rocket.chat/presence-service Patch
@rocket.chat/queue-worker Patch
@rocket.chat/abac Patch
@rocket.chat/federation-matrix Patch
@rocket.chat/license Patch
@rocket.chat/media-calls Patch
@rocket.chat/omnichannel-services Patch
@rocket.chat/pdf-worker Patch
@rocket.chat/models Patch
@rocket.chat/network-broker Patch
@rocket.chat/omni-core-ee Patch
@rocket.chat/mock-providers Patch
@rocket.chat/ui-video-conf Patch
@rocket.chat/instance-status Patch
@rocket.chat/omni-core Patch
@rocket.chat/server-fetch Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

Walkthrough

A patch is applied to fix unhandled promise rejections in AppListenerManager's post-event handlers. The change wraps async fire-and-forget post-event invocations with error handling via .catch(console.error), preventing UnhandledPromiseRejection errors while maintaining non-blocking behavior.

Changes

Cohort / File(s) Summary
Changelog
.changeset/fix-apps-engine-post-event-unhandled-promises.md
Documents the fix for unhandled promise rejections in AppListenerManager fire-and-forget post-event handlers (Node.js 15+ compatibility).
AppListenerManager Implementation
packages/apps-engine/src/server/managers/AppListenerManager.ts
Wraps eight post-event handler invocations (IPostMessageSent, IPostSystemMessageSent, IPostMessageDeleted, IPostMessageUpdated, IPostRoomCreate, IPostRoomDeleted, IPostExternalComponentOpened, IPostExternalComponentClosed) with .catch(console.error) to handle promise rejections without blocking execution.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested labels

type: bug

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: adding rejection handlers to fire-and-forget post-event dispatches in AppListenerManager to fix unhandled promise issues.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/apps-engine/src/server/managers/AppListenerManager.ts (1)

359-418: Consider centralizing the repeated fire-and-forget error handling.

The same void ... .catch(console.error) pattern is duplicated across eight branches. A small helper reduces drift and lets you include event context consistently in logs.

♻️ Proposed refactor
 export class AppListenerManager {
+	private fireAndForget(event: AppInterface, promise: Promise<void>): void {
+		void promise.catch((error) => console.error(`[AppListenerManager] ${event} failed`, error));
+	}
+
 	public async executeListener<I extends keyof IListenerExecutor>(
 		int: I,
 		data: IListenerExecutor[I]['args'][0],
 	): Promise<IListenerExecutor[I]['result']> {
@@
 			case AppInterface.IPostMessageSent:
-				void this.executePostMessageSent(data as IMessage).catch(console.error);
+				this.fireAndForget(AppInterface.IPostMessageSent, this.executePostMessageSent(data as IMessage));
 				return;
@@
 			case AppInterface.IPostExternalComponentClosed:
-				void this.executePostExternalComponentClosed(data as IExternalComponent).catch(console.error);
+				this.fireAndForget(
+					AppInterface.IPostExternalComponentClosed,
+					this.executePostExternalComponentClosed(data as IExternalComponent),
+				);
 				return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/apps-engine/src/server/managers/AppListenerManager.ts` around lines
359 - 418, Several branches repeat the "void X(...).catch(console.error)"
fire-and-forget pattern; extract a small helper (e.g., runFireAndForget,
safeExecute, or this.executeAsyncAndLog) that accepts a Promise-returning
invocation, an event name, and the payload, invokes the promise and centrally
handles .catch to log a contextual message; replace calls to
executePostMessageSent, executePostSystemMessageSent, executePostMessageDelete,
executePostMessageUpdated, executePostRoomCreate, executePostRoomDeleted,
executePostExternalComponentOpened, and executePostExternalComponentClosed with
this helper so all async "fire-and-forget" handlers use consistent error logging
including the event name and payload/context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/apps-engine/src/server/managers/AppListenerManager.ts`:
- Around line 359-418: Several branches repeat the "void
X(...).catch(console.error)" fire-and-forget pattern; extract a small helper
(e.g., runFireAndForget, safeExecute, or this.executeAsyncAndLog) that accepts a
Promise-returning invocation, an event name, and the payload, invokes the
promise and centrally handles .catch to log a contextual message; replace calls
to executePostMessageSent, executePostSystemMessageSent,
executePostMessageDelete, executePostMessageUpdated, executePostRoomCreate,
executePostRoomDeleted, executePostExternalComponentOpened, and
executePostExternalComponentClosed with this helper so all async
"fire-and-forget" handlers use consistent error logging including the event name
and payload/context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1b959c77-7778-4910-b5bc-8bcce3f48a10

📥 Commits

Reviewing files that changed from the base of the PR and between b8b8611 and b35c872.

📒 Files selected for processing (2)
  • .changeset/fix-apps-engine-post-event-unhandled-promises.md
  • packages/apps-engine/src/server/managers/AppListenerManager.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/playwright.mdc)

**/*.{ts,tsx,js}: Write concise, technical TypeScript/JavaScript with accurate typing in Playwright tests
Avoid code comments in the implementation

Files:

  • packages/apps-engine/src/server/managers/AppListenerManager.ts
🧠 Learnings (5)
📚 Learning: 2026-02-24T19:09:09.561Z
Learnt from: ahmed-n-abdeltwab
Repo: RocketChat/Rocket.Chat PR: 38974
File: apps/meteor/app/api/server/v1/im.ts:220-221
Timestamp: 2026-02-24T19:09:09.561Z
Learning: In RocketChat/Rocket.Chat OpenAPI migration PRs for apps/meteor/app/api/server/v1 endpoints, maintainers prefer to avoid any logic changes; style-only cleanups (like removing inline comments) may be deferred to follow-ups to keep scope tight.

Applied to files:

  • .changeset/fix-apps-engine-post-event-unhandled-promises.md
📚 Learning: 2026-02-24T19:05:56.710Z
Learnt from: ahmed-n-abdeltwab
Repo: RocketChat/Rocket.Chat PR: 0
File: :0-0
Timestamp: 2026-02-24T19:05:56.710Z
Learning: Rocket.Chat repo context: When a workspace manifest on develop already pins a dependency version (e.g., packages/web-ui-registration → "rocket.chat/ui-contexts": "27.0.1"), a lockfile change in a feature PR that upgrades only that dependency’s resolution is considered a manifest-driven sync and can be kept, preferably as a small "chore: sync yarn.lock with manifests" commit.

Applied to files:

  • .changeset/fix-apps-engine-post-event-unhandled-promises.md
📚 Learning: 2026-02-20T09:04:55.725Z
Learnt from: Shreyas2004wagh
Repo: RocketChat/Rocket.Chat PR: 38681
File: apps/meteor/server/modules/streamer/streamer.module.ts:307-313
Timestamp: 2026-02-20T09:04:55.725Z
Learning: In apps/meteor/server/modules/streamer/streamer.module.ts, the catch block in sendToManySubscriptions intentionally uses SystemLogger.debug (not error or warn) for per-subscription delivery failures to keep logs less noisy, as this was a deliberate design choice reviewed and approved by KevLehman.

Applied to files:

  • packages/apps-engine/src/server/managers/AppListenerManager.ts
📚 Learning: 2026-02-26T19:25:44.063Z
Learnt from: gabriellsh
Repo: RocketChat/Rocket.Chat PR: 38778
File: packages/ui-voip/src/providers/useMediaSession.ts:192-192
Timestamp: 2026-02-26T19:25:44.063Z
Learning: In the Rocket.Chat repository, do not reference Biome lint rules in code review feedback. Biome is not used even if biome.json exists; only reference Biome rules if there is explicit, project-wide usage documented. For TypeScript files, review lint implications without Biome guidance unless the project enables Biome rules.

Applied to files:

  • packages/apps-engine/src/server/managers/AppListenerManager.ts
📚 Learning: 2026-02-26T19:25:44.063Z
Learnt from: gabriellsh
Repo: RocketChat/Rocket.Chat PR: 38778
File: packages/ui-voip/src/providers/useMediaSession.ts:192-192
Timestamp: 2026-02-26T19:25:44.063Z
Learning: In this repository (RocketChat/Rocket.Chat), Biome lint rules are not used even if a biome.json exists. When reviewing TypeScript files (e.g., packages/ui-voip/src/providers/useMediaSession.ts), ensure lint suggestions do not reference Biome-specific rules. Rely on general ESLint/TypeScript lint rules and project conventions instead.

Applied to files:

  • packages/apps-engine/src/server/managers/AppListenerManager.ts
🔇 Additional comments (2)
packages/apps-engine/src/server/managers/AppListenerManager.ts (1)

359-418: Fix correctly hardens fire-and-forget post-event dispatches.

Attaching .catch(...) to each non-awaited post-event call preserves non-blocking behavior while preventing unhandled rejection crashes.

.changeset/fix-apps-engine-post-event-unhandled-promises.md (1)

1-5: Changeset entry matches the implementation and release scope.

Patch note clearly captures the fire-and-forget rejection handling fix and the impacted post-event hooks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant