Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/twelve-plums-turn.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<void> {
Expand All @@ -353,27 +359,41 @@ export class DenoRuntimeSubprocessController extends EventEmitter {
});
}

private waitForResponse(req: jsonrpc.RequestObject, options = this.options): Promise<unknown> {
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<unknown> } {
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 {
Expand Down
Loading