diff --git a/lib/repl.js b/lib/repl.js index 3c33c73c413006..e747cc581cc8eb 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -195,7 +195,12 @@ function setupExceptionCapture() { process.addUncaughtExceptionCaptureCallback((err) => { const store = replContext.getStore(); - if (store?.replServer && !store.replServer.closed) { + // TODO(addaleax): Add back a `store.replServer.closed` check here + // as a semver-major change. + // This check may need to allow for an opt-out, since the change in + // behavior could lead to DoS vulnerabilities (e.g. in the case of + // the net-based REPL described in our docs). + if (store?.replServer) { store.replServer._handleError(err); return true; // We handled it } diff --git a/test/parallel/test-repl-uncaught-exception-after-input-ended.js b/test/parallel/test-repl-uncaught-exception-after-input-ended.js new file mode 100644 index 00000000000000..1e2ca86a9f079c --- /dev/null +++ b/test/parallel/test-repl-uncaught-exception-after-input-ended.js @@ -0,0 +1,23 @@ +'use strict'; +const common = require('../common'); +const { start } = require('node:repl'); +const { PassThrough } = require('node:stream'); +const assert = require('node:assert'); + +// This test verifies that uncaught exceptions in the REPL +// do not bring down the process, even if stdin may already +// have been ended at that point (and the REPL closed as +// a result of that). +const input = new PassThrough(); +const output = new PassThrough().setEncoding('utf8'); +start({ + input, + output, + terminal: false, +}); + +input.end('setImmediate(() => { throw new Error("test"); });\n'); + +setImmediate(common.mustCall(() => { + assert.match(output.read(), /Uncaught Error: test/); +}));