diff --git a/.changeset/twelve-plums-turn.md b/.changeset/twelve-plums-turn.md new file mode 100644 index 0000000000000..be2a4b031e153 --- /dev/null +++ b/.changeset/twelve-plums-turn.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': patch +'@rocket.chat/apps-engine': patch +--- + +Fixes an issue that would cause the chat server to crash with an unhandled rejection in some cases diff --git a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts index 34f808aca7f0c..60f2cafed32b5 100644 --- a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts +++ b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts @@ -85,6 +85,8 @@ export type DenoRuntimeOptions = { timeout: number; }; +type AbortFunction = (reason?: any) => void; + export class DenoRuntimeSubprocessController extends EventEmitter { private deno: child_process.ChildProcess | undefined; @@ -322,13 +324,17 @@ export class DenoRuntimeSubprocessController extends EventEmitter { const request = jsonrpc.request(id, message.method, message.params); - const promise = this.waitForResponse(request, options).finally(() => { - this.debug('Request %s for method %s took %dms', id, message.method, Date.now() - start); - }); + const { promise, abort } = this.waitForResponse(request, options); - this.messenger.send(request); + try { + this.messenger.send(request); + } catch (e) { + abort(e); + } - return promise; + return promise.finally(() => { + this.debug('Request %s for method %s took %dms', id, message.method, Date.now() - start); + }); } private waitUntilReady(): Promise { @@ -353,27 +359,41 @@ export class DenoRuntimeSubprocessController extends EventEmitter { }); } - private waitForResponse(req: jsonrpc.RequestObject, options = this.options): Promise { - return new Promise((resolve, reject) => { - const responseCallback = (result: unknown, error: jsonrpc.IParsedObjectError['payload']['error']) => { - clearTimeout(timeoutId); + private waitForResponse(req: jsonrpc.RequestObject, options = this.options): { abort: AbortFunction; promise: Promise } { + const controller = new AbortController(); + const { abort, signal } = controller; - if (error) { - reject(error); - } + return { + abort: abort.bind(controller), + promise: new Promise((resolve, reject) => { + const eventName = `result:${req.id}`; - resolve(result); - }; + const responseCallback = (result: unknown, error: jsonrpc.IParsedObjectError['payload']['error'] | Error) => { + this.off(eventName, responseCallback); + clearTimeout(timeoutId); + + if (error) { + reject(error); + } - const eventName = `result:${req.id}`; + resolve(result); + }; - const timeoutId = setTimeout(() => { - this.off(eventName, responseCallback); - reject(new Error(`[${this.getAppId()}] Request "${req.id}" for method "${req.method}" timed out`)); - }, options.timeout); + const timeoutId = setTimeout( + () => + responseCallback( + undefined, + new Error(`[${this.getAppId()}] Request "${req.id}" for method "${req.method}" timed out after ${options.timeout}ms`), + ), + options.timeout, + ); - this.once(eventName, responseCallback); - }); + signal.onabort = () => + responseCallback(undefined, signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason))); + + this.once(eventName, responseCallback); + }), + }; } private onReady(): void {