From 27b77a44dd1a221acea87ec691d6a0952813ee7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1aki=20Baz=20Castillo?= Date: Mon, 30 May 2022 11:42:23 +0200 Subject: [PATCH 1/8] Sentry Node: do not exit process if the app added its own 'uncaughtException' or 'unhandledRejection' listener - Fixes #1661 --- packages/node/src/integrations/onuncaughtexception.ts | 11 +++++++---- .../node/src/integrations/onunhandledrejection.ts | 8 +++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index dfff26fa0675..d2b2c8303937 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -58,6 +58,9 @@ export class OnUncaughtException implements Integration { return (error: Error): void => { let onFatalError: OnFatalErrorHandler = logAndExitProcess; const client = getCurrentHub().getClient(); + // in order to honour Node's original behavior on uncaught exceptions, we should not + // exit the process if the app added its own 'uncaughtException' listener + const shouldExitProcess: boolean = global.process.listenerCount('uncaughtException') === 1; if (this._options.onFatalError) { // eslint-disable-next-line @typescript-eslint/unbound-method @@ -83,23 +86,23 @@ export class OnUncaughtException implements Integration { originalException: error, data: { mechanism: { handled: false, type: 'onuncaughtexception' } }, }); - if (!calledFatalError) { + if (!calledFatalError && shouldExitProcess) { calledFatalError = true; onFatalError(error); } }); } else { - if (!calledFatalError) { + if (!calledFatalError && shouldExitProcess) { calledFatalError = true; onFatalError(error); } } - } else if (calledFatalError) { + } else if (calledFatalError && shouldExitProcess) { // we hit an error *after* calling onFatalError - pretty boned at this point, just shut it down IS_DEBUG_BUILD && logger.warn('uncaught exception after calling fatal error shutdown callback - this is bad! forcing shutdown'); logAndExitProcess(error); - } else if (!caughtSecondError) { + } else if (!caughtSecondError && shouldExitProcess) { // two cases for how we can hit this branch: // - capturing of first error blew up and we just caught the exception from that // - quit trying to capture, proceed with shutdown diff --git a/packages/node/src/integrations/onunhandledrejection.ts b/packages/node/src/integrations/onunhandledrejection.ts index 2ba9a3d8f205..684049edc845 100644 --- a/packages/node/src/integrations/onunhandledrejection.ts +++ b/packages/node/src/integrations/onunhandledrejection.ts @@ -70,6 +70,10 @@ export class OnUnhandledRejection implements Integration { 'or by rejecting a promise which was not handled with .catch().' + ' The promise rejected with the reason:'; + // in order to honour Node's original behavior on unhandled rejections, we should not + // exit the process if the app added its own 'unhandledRejection' listener + const shouldExitProcess: boolean = global.process.listenerCount('unhandledRejection') === 1; + /* eslint-disable no-console */ if (this._options.mode === 'warn') { consoleSandbox(() => { @@ -81,7 +85,9 @@ export class OnUnhandledRejection implements Integration { consoleSandbox(() => { console.warn(rejectionWarning); }); - logAndExitProcess(reason); + if (shouldExitProcess) { + logAndExitProcess(reason); + } } /* eslint-enable no-console */ } From ba449542f2e5b9024e3a7c2f43784b65d8194dde Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 30 May 2022 16:10:07 +0000 Subject: [PATCH 2/8] Add integration tests --- .../additional-listener-test-script.js | 16 ++++++++++ .../no-additional-listener-test-script.js | 13 +++++++++ .../integrations/OnUncaughtException/test.ts | 29 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/additional-listener-test-script.js create mode 100644 packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/no-additional-listener-test-script.js create mode 100644 packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/test.ts diff --git a/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/additional-listener-test-script.js b/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/additional-listener-test-script.js new file mode 100644 index 000000000000..033bb077b1cd --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/additional-listener-test-script.js @@ -0,0 +1,16 @@ +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); + +process.on('uncaughtException', () => { + // do nothing - this will prevent the Error below from closing this process before the timeout resolves +}); + +setTimeout(() => { + process.stdout.write("I'm alive!"); + process.exit(0); +}, 500); + +throw new Error(); diff --git a/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/no-additional-listener-test-script.js b/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/no-additional-listener-test-script.js new file mode 100644 index 000000000000..fcff5962b629 --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/no-additional-listener-test-script.js @@ -0,0 +1,13 @@ +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); + +setTimeout(() => { + // This should not be called because the script throws before this resolves + process.stdout.write("I'm alive!"); + process.exit(0); +}, 500); + +throw new Error(); diff --git a/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/test.ts b/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/test.ts new file mode 100644 index 000000000000..5fd8f1e4da9f --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/test.ts @@ -0,0 +1,29 @@ +import * as childProcess from 'child_process'; +import * as path from 'path'; + +describe('OnUncaughtException integration', () => { + test('should close process on uncaught error with no additional listeners registered', done => { + expect.assertions(3); + + const testScriptPath = path.resolve(__dirname, 'no-additional-listener-test-script.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stdout) => { + expect(err).not.toBeNull(); + expect(err?.code).toBe(1); + expect(stdout).not.toBe("I'm alive!"); + done(); + }); + }); + + test('should not close process on uncaught error when additional listeners are registered', done => { + expect.assertions(2); + + const testScriptPath = path.resolve(__dirname, 'additional-listener-test-script.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stdout) => { + expect(err).toBeNull(); + expect(stdout).toBe("I'm alive!"); + done(); + }); + }); +}); From 99a9633634d19b0a400c25d2ebda70f5cb09236b Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 31 May 2022 08:12:57 +0000 Subject: [PATCH 3/8] Improve documentation --- .../src/integrations/onuncaughtexception.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index d2b2c8303937..967269a54ebf 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -8,6 +8,18 @@ import { logAndExitProcess } from './utils/errorhandling'; type OnFatalErrorHandler = (firstError: Error, secondError?: Error) => void; +interface OnUncaughtExceptionOptions { + /** + * This is called when an uncaught error would cause the process to exit. + * + * @param firstError Uncaught error causing the process to exit + * @param secondError Will be set if the handler was called multiple times. This can happen either because + * `onFatalError` itself threw, or because an independent error happened somewhere else while `onFatalError` + * was running. + */ + onFatalError?(firstError: Error, secondError?: Error): void; +} + /** Global Promise Rejection handler */ export class OnUncaughtException implements Integration { /** @@ -28,16 +40,7 @@ export class OnUncaughtException implements Integration { /** * @inheritDoc */ - public constructor( - private readonly _options: { - /** - * Default onFatalError handler - * @param firstError Error that has been thrown - * @param secondError If this was called multiple times this will be set - */ - onFatalError?(firstError: Error, secondError?: Error): void; - } = {}, - ) {} + public constructor(private readonly _options: OnUncaughtExceptionOptions = {}) {} /** * @inheritDoc */ From 8f01e8378093968c474be2da657fe5258962994c Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 31 May 2022 08:13:09 +0000 Subject: [PATCH 4/8] Fix behaviour in combination with domains --- .../src/integrations/onuncaughtexception.ts | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index 967269a54ebf..0a8448bdd668 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -35,7 +35,7 @@ export class OnUncaughtException implements Integration { /** * @inheritDoc */ - public readonly handler: (error: Error) => void = this._makeErrorHandler(); + public readonly handler: (error: Error) => void = this._makeErrorHandler().bind(this); /** * @inheritDoc @@ -45,7 +45,7 @@ export class OnUncaughtException implements Integration { * @inheritDoc */ public setupOnce(): void { - global.process.on('uncaughtException', this.handler.bind(this)); + global.process.on('uncaughtException', this.handler); } /** @@ -61,9 +61,26 @@ export class OnUncaughtException implements Integration { return (error: Error): void => { let onFatalError: OnFatalErrorHandler = logAndExitProcess; const client = getCurrentHub().getClient(); - // in order to honour Node's original behavior on uncaught exceptions, we should not - // exit the process if the app added its own 'uncaughtException' listener - const shouldExitProcess: boolean = global.process.listenerCount('uncaughtException') === 1; + + // Attaching a listener to `uncaughtException` will prevent the node process from exiting. We generally do not + // want to alter this behaviour so we check for other listeners that users may have attached themselves and adjust + // exit behaviour of the SDK accordingly: + // - If other listeners are attached, do not exit. + // - If the only listener attached is ours, exit. + const userProvidedListenersCount = global.process + .listeners('uncaughtException') + .reduce((acc, listener) => { + if ( + listener.name === 'domainUncaughtExceptionClear' || // as soon as we're using domains this listener is attached by node itself + listener === this.handler // filter the handler we registered ourselves) + ) { + return acc; + } else { + return acc + 1; + } + }, 0); + + const shouldExitProcess: boolean = userProvidedListenersCount === 0; if (this._options.onFatalError) { // eslint-disable-next-line @typescript-eslint/unbound-method From 9fee35e1d5ec4b1cad123b2b272f52c0d23ab1df Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 31 May 2022 10:12:55 +0000 Subject: [PATCH 5/8] Add `mimicNativeBehaviour` option to OnUncaughtException --- ...haviour-additional-listener-test-script.js | 27 ++++++++++++++ ...iour-no-additional-listener-test-script.js | 24 +++++++++++++ .../integrations/OnUncaughtException/test.ts | 36 ++++++++++++++++--- .../src/integrations/onuncaughtexception.ts | 22 ++++++++++-- 4 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/mimic-behaviour-additional-listener-test-script.js create mode 100644 packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/mimic-behaviour-no-additional-listener-test-script.js diff --git a/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/mimic-behaviour-additional-listener-test-script.js b/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/mimic-behaviour-additional-listener-test-script.js new file mode 100644 index 000000000000..e9c53bddc1fb --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/mimic-behaviour-additional-listener-test-script.js @@ -0,0 +1,27 @@ +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: integrations => { + return integrations.map(integration => { + if (integration.name === 'OnUncaughtException') { + return new Sentry.Integrations.OnUncaughtException({ + mimicNativeBehaviour: true, + }); + } else { + return integration; + } + }); + }, +}); + +process.on('uncaughtException', () => { + // do nothing - this will prevent the Error below from closing this process before the timeout resolves +}); + +setTimeout(() => { + process.stdout.write("I'm alive!"); + process.exit(0); +}, 500); + +throw new Error(); diff --git a/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/mimic-behaviour-no-additional-listener-test-script.js b/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/mimic-behaviour-no-additional-listener-test-script.js new file mode 100644 index 000000000000..7b3307db4211 --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/mimic-behaviour-no-additional-listener-test-script.js @@ -0,0 +1,24 @@ +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: integrations => { + return integrations.map(integration => { + if (integration.name === 'OnUncaughtException') { + return new Sentry.Integrations.OnUncaughtException({ + mimicNativeBehaviour: true, + }); + } else { + return integration; + } + }); + }, +}); + +setTimeout(() => { + // This should not be called because the script throws before this resolves + process.stdout.write("I'm alive!"); + process.exit(0); +}, 500); + +throw new Error(); diff --git a/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/test.ts b/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/test.ts index 5fd8f1e4da9f..3c656443fc96 100644 --- a/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/test.ts +++ b/packages/node-integration-tests/suites/public-api/integrations/OnUncaughtException/test.ts @@ -15,15 +15,43 @@ describe('OnUncaughtException integration', () => { }); }); - test('should not close process on uncaught error when additional listeners are registered', done => { - expect.assertions(2); + test('should close process on uncaught error when additional listeners are registered', done => { + expect.assertions(3); const testScriptPath = path.resolve(__dirname, 'additional-listener-test-script.js'); childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stdout) => { - expect(err).toBeNull(); - expect(stdout).toBe("I'm alive!"); + expect(err).not.toBeNull(); + expect(err?.code).toBe(1); + expect(stdout).not.toBe("I'm alive!"); done(); }); }); + + describe('with `mimicNativeBehaviour` set to true', () => { + test('should close process on uncaught error with no additional listeners registered', done => { + expect.assertions(3); + + const testScriptPath = path.resolve(__dirname, 'mimic-behaviour-no-additional-listener-test-script.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stdout) => { + expect(err).not.toBeNull(); + expect(err?.code).toBe(1); + expect(stdout).not.toBe("I'm alive!"); + done(); + }); + }); + + test('should not close process on uncaught error when additional listeners are registered', done => { + expect.assertions(2); + + const testScriptPath = path.resolve(__dirname, 'mimic-behaviour-additional-listener-test-script.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stdout) => { + expect(err).toBeNull(); + expect(stdout).toBe("I'm alive!"); + done(); + }); + }); + }); }); diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index 0a8448bdd668..e69c9829c354 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -9,6 +9,16 @@ import { logAndExitProcess } from './utils/errorhandling'; type OnFatalErrorHandler = (firstError: Error, secondError?: Error) => void; interface OnUncaughtExceptionOptions { + // TODO(v8): Evaluate whether we should switch the default behaviour here. + // Also, we can evaluate using https://nodejs.org/api/process.html#event-uncaughtexceptionmonitor per default, and + // falling back to current behaviour when that's not available. + /** + * Whether the SDK should mimic native behaviour when a global error occurs. Default: `false`. + * - `false`: The SDK will exit the process on all uncaught errors. + * - `true`: The SDK will only exit the process when there are no other 'uncaughtException' handlers attached. + */ + mimicNativeBehaviour: boolean; + /** * This is called when an uncaught error would cause the process to exit. * @@ -37,10 +47,18 @@ export class OnUncaughtException implements Integration { */ public readonly handler: (error: Error) => void = this._makeErrorHandler().bind(this); + private readonly _options: OnUncaughtExceptionOptions; + /** * @inheritDoc */ - public constructor(private readonly _options: OnUncaughtExceptionOptions = {}) {} + public constructor(options: Partial = {}) { + this._options = { + mimicNativeBehaviour: false, + ...options, + }; + } + /** * @inheritDoc */ @@ -80,7 +98,7 @@ export class OnUncaughtException implements Integration { } }, 0); - const shouldExitProcess: boolean = userProvidedListenersCount === 0; + const shouldExitProcess: boolean = !this._options.mimicNativeBehaviour || userProvidedListenersCount === 0; if (this._options.onFatalError) { // eslint-disable-next-line @typescript-eslint/unbound-method From faad12841fdb0f46f3f4f14833a4578cf73db8e5 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 31 May 2022 12:15:06 +0000 Subject: [PATCH 6/8] Adjust docstring --- packages/node/src/integrations/onuncaughtexception.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index e69c9829c354..e9c25c4e3f02 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -13,7 +13,7 @@ interface OnUncaughtExceptionOptions { // Also, we can evaluate using https://nodejs.org/api/process.html#event-uncaughtexceptionmonitor per default, and // falling back to current behaviour when that's not available. /** - * Whether the SDK should mimic native behaviour when a global error occurs. Default: `false`. + * Whether the SDK should mimic native behaviour when a global error occurs. Default: `false` * - `false`: The SDK will exit the process on all uncaught errors. * - `true`: The SDK will only exit the process when there are no other 'uncaughtException' handlers attached. */ From 44ae1dbb6c11cad4f1c0f9bd6c3266a95c9dcfc8 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 31 May 2022 12:19:19 +0000 Subject: [PATCH 7/8] Clean up OnUnhandledRejection integration --- .../src/integrations/onunhandledrejection.ts | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/node/src/integrations/onunhandledrejection.ts b/packages/node/src/integrations/onunhandledrejection.ts index 684049edc845..74148da79ee0 100644 --- a/packages/node/src/integrations/onunhandledrejection.ts +++ b/packages/node/src/integrations/onunhandledrejection.ts @@ -4,7 +4,16 @@ import { consoleSandbox } from '@sentry/utils'; import { logAndExitProcess } from './utils/errorhandling'; -type UnhandledRejectionMode = 'none' | 'warn' | 'strict'; +interface OnUnhandledRejectionOptions { + // TODO(v8): Evaluate whether we should mimic nodes default behaviour for this integration and/or remove the mode option. + // Also, we can evaluate using https://nodejs.org/api/process.html#event-uncaughtexceptionmonitor per default, and + // falling back to current behaviour when that's not available. + /** + * Option deciding what to do after capturing unhandledRejection, + * that mimicks behavior of node's --unhandled-rejection flag. Default: `'warn'` + */ + mode: 'none' | 'warn' | 'strict'; +} /** Global Promise Rejection handler */ export class OnUnhandledRejection implements Integration { @@ -18,18 +27,17 @@ export class OnUnhandledRejection implements Integration { */ public name: string = OnUnhandledRejection.id; + private readonly _options: OnUnhandledRejectionOptions; + /** * @inheritDoc */ - public constructor( - private readonly _options: { - /** - * Option deciding what to do after capturing unhandledRejection, - * that mimicks behavior of node's --unhandled-rejection flag. - */ - mode: UnhandledRejectionMode; - } = { mode: 'warn' }, - ) {} + public constructor(options: Partial = {}) { + this._options = { + mode: 'warn', + ...options, + }; + } /** * @inheritDoc @@ -70,10 +78,6 @@ export class OnUnhandledRejection implements Integration { 'or by rejecting a promise which was not handled with .catch().' + ' The promise rejected with the reason:'; - // in order to honour Node's original behavior on unhandled rejections, we should not - // exit the process if the app added its own 'unhandledRejection' listener - const shouldExitProcess: boolean = global.process.listenerCount('unhandledRejection') === 1; - /* eslint-disable no-console */ if (this._options.mode === 'warn') { consoleSandbox(() => { @@ -85,9 +89,7 @@ export class OnUnhandledRejection implements Integration { consoleSandbox(() => { console.warn(rejectionWarning); }); - if (shouldExitProcess) { - logAndExitProcess(reason); - } + logAndExitProcess(reason); } /* eslint-enable no-console */ } From 8ef1dc8444d8fdffdb22c5000ed35f8b96c0b31e Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 31 May 2022 12:19:37 +0000 Subject: [PATCH 8/8] Add integration tests for OnUnhandledRejection integration --- .../OnUnhandledRejection/test-script.js | 29 +++++++++++++++ .../integrations/OnUnhandledRejection/test.ts | 36 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 packages/node-integration-tests/suites/public-api/integrations/OnUnhandledRejection/test-script.js create mode 100644 packages/node-integration-tests/suites/public-api/integrations/OnUnhandledRejection/test.ts diff --git a/packages/node-integration-tests/suites/public-api/integrations/OnUnhandledRejection/test-script.js b/packages/node-integration-tests/suites/public-api/integrations/OnUnhandledRejection/test-script.js new file mode 100644 index 000000000000..0ca2ea78c0e3 --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/integrations/OnUnhandledRejection/test-script.js @@ -0,0 +1,29 @@ +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: integrations => { + return integrations.map(integration => { + if (integration.name === 'OnUnhandledRejection') { + return new Sentry.Integrations.OnUnhandledRejection({ + mode: process.env['PROMISE_REJECTION_MODE'], + }); + } else { + return integration; + } + }); + }, +}); + +if (process.env['ATTACH_ADDITIONAL_HANDLER']) { + process.on('uncaughtException', () => { + // do nothing - this will prevent the rejected promise below from closing this process before the timeout resolves + }); +} + +setTimeout(() => { + process.stdout.write("I'm alive!"); + process.exit(0); +}, 500); + +Promise.reject(); diff --git a/packages/node-integration-tests/suites/public-api/integrations/OnUnhandledRejection/test.ts b/packages/node-integration-tests/suites/public-api/integrations/OnUnhandledRejection/test.ts new file mode 100644 index 000000000000..83e91d6f8c0f --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/integrations/OnUnhandledRejection/test.ts @@ -0,0 +1,36 @@ +import * as childProcess from 'child_process'; +import * as path from 'path'; + +describe('OnUnhandledRejection integration', () => { + test.each([ + { mode: 'none', additionalHandler: false, expectedErrorCode: 0 }, + { mode: 'warn', additionalHandler: false, expectedErrorCode: 0 }, + { mode: 'strict', additionalHandler: false, expectedErrorCode: 1 }, + + { mode: 'none', additionalHandler: true, expectedErrorCode: 0 }, + { mode: 'warn', additionalHandler: true, expectedErrorCode: 0 }, + { mode: 'strict', additionalHandler: true, expectedErrorCode: 1 }, + ])( + 'should cause process to exit with code $expectedErrorCode with `mode` set to $mode while having a handler attached (? - $additionalHandler)', + async ({ mode, additionalHandler, expectedErrorCode }) => { + const testScriptPath = path.resolve(__dirname, 'test-script.js'); + + await new Promise(resolve => { + childProcess.exec( + `node ${testScriptPath}`, + { + encoding: 'utf8', + env: { + PROMISE_REJECTION_MODE: mode ? String(mode) : undefined, + ATTACH_ADDITIONAL_HANDLER: additionalHandler ? String(additionalHandler) : undefined, + }, + }, + err => { + expect(err?.code).toBe(expectedErrorCode || undefined); + resolve(); + }, + ); + }); + }, + ); +});