diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md
index bb8ca3e63e0096..b47a17bd747d97 100644
--- a/doc/api/deprecations.md
+++ b/doc/api/deprecations.md
@@ -4430,6 +4430,42 @@ Passing the `type` option to [`Duplex.toWeb()`][] is deprecated. To specify the
type of the readable half of the constructed readable-writable pair, use the
`readableType` option instead.
+### DEP0202: `Http1IncomingMessage` and `Http1ServerResponse` options of HTTP/2 servers
+
+
+
+Type: Documentation-only
+
+The `Http1IncomingMessage` and `Http1ServerResponse` options of
+[`http2.createServer()`][] and [`http2.createSecureServer()`][] are
+deprecated. Use `http1Options.IncomingMessage` and
+`http1Options.ServerResponse` instead.
+
+```cjs
+// Deprecated
+const server = http2.createSecureServer({
+ allowHTTP1: true,
+ Http1IncomingMessage: MyIncomingMessage,
+ Http1ServerResponse: MyServerResponse,
+});
+```
+
+```cjs
+// Use this instead
+const server = http2.createSecureServer({
+ allowHTTP1: true,
+ http1Options: {
+ IncomingMessage: MyIncomingMessage,
+ ServerResponse: MyServerResponse,
+ },
+});
+```
+
[DEP0142]: #dep0142-repl_builtinlibs
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
[RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3
@@ -4509,6 +4545,8 @@ type of the readable half of the constructed readable-writable pair, use the
[`http.ServerResponse`]: http.md#class-httpserverresponse
[`http.get()`]: http.md#httpgetoptions-callback
[`http.request()`]: http.md#httprequestoptions-callback
+[`http2.createSecureServer()`]: http2.md#http2createsecureserveroptions-onrequesthandler
+[`http2.createServer()`]: http2.md#http2createserveroptions-onrequesthandler
[`https.get()`]: https.md#httpsgetoptions-callback
[`https.request()`]: https.md#httpsrequestoptions-callback
[`message.connection`]: http.md#messageconnection
diff --git a/doc/api/http2.md b/doc/api/http2.md
index 62a213f145d80e..4663c7400b3730 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -2796,6 +2796,10 @@ Throws `ERR_INVALID_ARG_TYPE` for invalid `settings` argument.
+
+* `data` {Object}
+ * `tests` {Array} An array of objects containing information about the
+ interrupted tests.
+ * `column` {number|undefined} The column number where the test is defined,
+ or `undefined` if the test was run through the REPL.
+ * `file` {string|undefined} The path of the test file,
+ `undefined` if test was run through the REPL.
+ * `line` {number|undefined} The line number where the test is defined, or
+ `undefined` if the test was run through the REPL.
+ * `name` {string} The test name.
+ * `nesting` {number} The nesting level of the test.
+
+Emitted when the test runner is interrupted by a `SIGINT` signal (e.g., when
+pressing Ctrl+C). The event contains information about
+the tests that were running at the time of interruption.
+
+When using process isolation (the default), the test name will be the file path
+since the parent runner only knows about file-level tests. When using
+`--test-isolation=none`, the actual test name is shown.
+
### Event: `'test:pass'`
* `data` {Object}
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 9f21daeff16bfd..0b30abde1ea094 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -48,8 +48,12 @@ const { Duplex } = require('stream');
const tls = require('tls');
const { setImmediate, setTimeout, clearTimeout } = require('timers');
-const { kIncomingMessage } = require('_http_common');
-const { kServerResponse, Server: HttpServer, httpServerPreClose, setupConnectionsTracking } = require('_http_server');
+const {
+ Server: HttpServer,
+ httpServerPreClose,
+ setupConnectionsTracking,
+ storeHTTPOptions,
+} = require('_http_server');
const JSStreamSocket = require('internal/js_stream_socket');
const {
@@ -3257,8 +3261,6 @@ function connectionListener(socket) {
if (socket.alpnProtocol === false || socket.alpnProtocol === 'http/1.1') {
// Fallback to HTTP/1.1
if (options.allowHTTP1 === true) {
- socket.server[kIncomingMessage] = options.Http1IncomingMessage;
- socket.server[kServerResponse] = options.Http1ServerResponse;
return httpConnectionListener.call(this, socket);
}
// Let event handler deal with the socket
@@ -3340,9 +3342,18 @@ function initializeOptions(options) {
options.unknownProtocolTimeout = 10000;
- // Used only with allowHTTP1
- options.Http1IncomingMessage ||= http.IncomingMessage;
- options.Http1ServerResponse ||= http.ServerResponse;
+ // Initialize http1Options bag for HTTP/1 fallback when allowHTTP1 is true.
+ // This bag is passed to storeHTTPOptions() to configure HTTP/1 server
+ // behavior (timeouts, IncomingMessage/ServerResponse classes, etc.).
+ options.http1Options = { ...options.http1Options };
+
+ // Backward compat: migrate deprecated top-level Http1 options (DEP0201)
+ if (options.Http1IncomingMessage !== undefined) {
+ options.http1Options.IncomingMessage ??= options.Http1IncomingMessage;
+ }
+ if (options.Http1ServerResponse !== undefined) {
+ options.http1Options.ServerResponse ??= options.Http1ServerResponse;
+ }
options.Http2ServerRequest ||= Http2ServerRequest;
options.Http2ServerResponse ||= Http2ServerResponse;
@@ -3390,9 +3401,7 @@ class Http2SecureServer extends TLSServer {
this.timeout = 0;
this.on('newListener', setupCompat);
if (options.allowHTTP1 === true) {
- this.headersTimeout = 60_000; // Minimum between 60 seconds or requestTimeout
- this.requestTimeout = 300_000; // 5 minutes
- this.connectionsCheckingInterval = 30_000; // 30 seconds
+ storeHTTPOptions.call(this, { ...options, ...options.http1Options });
this.shouldUpgradeCallback = function() {
return this.listenerCount('upgrade') > 0;
};
diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js
index 225436661f5e56..06c2c8602da444 100644
--- a/lib/internal/main/watch_mode.js
+++ b/lib/internal/main/watch_mode.js
@@ -18,7 +18,7 @@ const {
triggerUncaughtException,
exitCodes: { kNoFailure },
} = internalBinding('errors');
-const { getOptionValue, getOptionsAsFlagsFromBinding } = require('internal/options');
+const { getOptionValue } = require('internal/options');
const { FilesWatcher } = require('internal/watch_mode/files_watcher');
const { green, blue, red, white, clear } = require('internal/util/colors');
const { convertToValidSignal } = require('internal/util');
@@ -44,14 +44,13 @@ const kCommand = ArrayPrototypeSlice(process.argv, 1);
const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' '));
const argsWithoutWatchOptions = [];
-const argsFromBinding = getOptionsAsFlagsFromBinding();
-for (let i = 0; i < argsFromBinding.length; i++) {
- const arg = argsFromBinding[i];
+for (let i = 0; i < process.execArgv.length; i++) {
+ const arg = process.execArgv[i];
if (StringPrototypeStartsWith(arg, '--watch=')) {
continue;
}
if (arg === '--watch') {
- const nextArg = argsFromBinding[i + 1];
+ const nextArg = process.execArgv[i + 1];
if (nextArg && nextArg[0] !== '-') {
// If `--watch` doesn't include `=` and the next
// argument is not a flag then it is interpreted as
diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js
index 6b3b13b2c88d65..5418a14a4410a4 100644
--- a/lib/internal/test_runner/harness.js
+++ b/lib/internal/test_runner/harness.js
@@ -3,6 +3,7 @@ const {
ArrayPrototypeForEach,
ArrayPrototypePush,
FunctionPrototypeBind,
+ Promise,
PromiseResolve,
PromiseWithResolvers,
SafeMap,
@@ -32,7 +33,7 @@ const { PassThrough, compose } = require('stream');
const { reportReruns } = require('internal/test_runner/reporter/rerun');
const { queueMicrotask } = require('internal/process/task_queues');
const { TIMEOUT_MAX } = require('internal/timers');
-const { clearInterval, setInterval } = require('timers');
+const { clearInterval, setImmediate, setInterval } = require('timers');
const { bigint: hrtime } = process.hrtime;
const testResources = new SafeMap();
let globalRoot;
@@ -289,7 +290,33 @@ function setupProcessState(root, globalOptions) {
}
};
+ const findRunningTests = (test, running = []) => {
+ if (test.startTime !== null && !test.finished) {
+ for (let i = 0; i < test.subtests.length; i++) {
+ findRunningTests(test.subtests[i], running);
+ }
+ // Only add leaf tests (innermost running tests)
+ if (test.activeSubtests === 0 && test.name !== '') {
+ ArrayPrototypePush(running, {
+ __proto__: null,
+ name: test.name,
+ nesting: test.nesting,
+ file: test.loc?.file,
+ line: test.loc?.line,
+ column: test.loc?.column,
+ });
+ }
+ }
+ return running;
+ };
+
const terminationHandler = async () => {
+ const runningTests = findRunningTests(root);
+ if (runningTests.length > 0) {
+ root.reporter.interrupted(runningTests);
+ // Allow the reporter stream to process the interrupted event
+ await new Promise((resolve) => setImmediate(resolve));
+ }
await exitHandler(true);
process.exit();
};
diff --git a/lib/internal/test_runner/reporter/spec.js b/lib/internal/test_runner/reporter/spec.js
index 14c447f316492f..fce0754e25061a 100644
--- a/lib/internal/test_runner/reporter/spec.js
+++ b/lib/internal/test_runner/reporter/spec.js
@@ -106,8 +106,31 @@ class SpecReporter extends Transform {
break;
case 'test:watch:restarted':
return `\nRestarted at ${DatePrototypeToLocaleString(new Date())}\n`;
+ case 'test:interrupted':
+ return this.#formatInterruptedTests(data.tests);
}
}
+ #formatInterruptedTests(tests) {
+ if (tests.length === 0) {
+ return '';
+ }
+
+ const results = [
+ `\n${colors.yellow}Interrupted while running:${colors.white}\n`,
+ ];
+
+ for (let i = 0; i < tests.length; i++) {
+ const test = tests[i];
+ let msg = `${indent(test.nesting)}${reporterUnicodeSymbolMap['warning:alert']}${test.name}`;
+ if (test.file) {
+ const relPath = relative(this.#cwd, test.file);
+ msg += ` ${colors.gray}(${relPath}:${test.line}:${test.column})${colors.white}`;
+ }
+ ArrayPrototypePush(results, msg);
+ }
+
+ return ArrayPrototypeJoin(results, '\n') + '\n';
+ }
_transform({ type, data }, encoding, callback) {
callback(null, this.#handleEvent({ __proto__: null, type, data }));
}
diff --git a/lib/internal/test_runner/reporter/tap.js b/lib/internal/test_runner/reporter/tap.js
index 01c698871b9134..5d25fdda15959f 100644
--- a/lib/internal/test_runner/reporter/tap.js
+++ b/lib/internal/test_runner/reporter/tap.js
@@ -61,6 +61,16 @@ async function * tapReporter(source) {
case 'test:coverage':
yield getCoverageReport(indent(data.nesting), data.summary, '# ', '', true);
break;
+ case 'test:interrupted':
+ for (let i = 0; i < data.tests.length; i++) {
+ const test = data.tests[i];
+ let msg = `Interrupted while running: ${test.name}`;
+ if (test.file) {
+ msg += ` at ${test.file}:${test.line}:${test.column}`;
+ }
+ yield `# ${tapEscape(msg)}\n`;
+ }
+ break;
}
}
}
diff --git a/lib/internal/test_runner/tests_stream.js b/lib/internal/test_runner/tests_stream.js
index 7b64487696f53f..17b6890b5fc5df 100644
--- a/lib/internal/test_runner/tests_stream.js
+++ b/lib/internal/test_runner/tests_stream.js
@@ -149,6 +149,13 @@ class TestsStream extends Readable {
});
}
+ interrupted(tests) {
+ this[kEmitMessage]('test:interrupted', {
+ __proto__: null,
+ tests,
+ });
+ }
+
end() {
this.#tryPush(null);
}
diff --git a/test/parallel/test-http2-https-fallback-http-server-options.js b/test/parallel/test-http2-https-fallback-http-server-options.js
index 8143f56d491ccc..29bb24d1cfd242 100644
--- a/test/parallel/test-http2-https-fallback-http-server-options.js
+++ b/test/parallel/test-http2-https-fallback-http-server-options.js
@@ -20,6 +20,10 @@ const ca = fixtures.readKey('fake-startcom-root-cert.pem');
function onRequest(request, response) {
const { socket: { alpnProtocol } } = request.httpVersion === '2.0' ?
request.stream.session : request;
+ // Verify that http1Options are applied when allowHTTP1 is true
+ if (request.httpVersion === '1.1') {
+ assert.strictEqual(request.socket.server.keepAliveTimeout, 10000);
+ }
response.status(200);
response.end(JSON.stringify({
alpnProtocol,
@@ -46,8 +50,11 @@ class MyServerResponse extends http.ServerResponse {
{
cert,
key, allowHTTP1: true,
- Http1IncomingMessage: MyIncomingMessage,
- Http1ServerResponse: MyServerResponse
+ http1Options: {
+ IncomingMessage: MyIncomingMessage,
+ ServerResponse: MyServerResponse,
+ keepAliveTimeout: 10000,
+ },
},
common.mustCall(onRequest, 1)
);
diff --git a/test/parallel/test-runner-exit-code.js b/test/parallel/test-runner-exit-code.js
index 792c5f1717bd60..4024a52841bb28 100644
--- a/test/parallel/test-runner-exit-code.js
+++ b/test/parallel/test-runner-exit-code.js
@@ -6,7 +6,7 @@ const { spawnSync, spawn } = require('child_process');
const { once } = require('events');
const { finished } = require('stream/promises');
-async function runAndKill(file) {
+async function runAndKill(file, expectedTestName) {
if (common.isWindows) {
common.printSkipMessage(`signals are not supported in windows, skipping ${file}`);
return;
@@ -21,6 +21,9 @@ async function runAndKill(file) {
const [code, signal] = await once(child, 'exit');
await finished(child.stdout);
assert(stdout.startsWith('TAP version 13\n'));
+ // Verify interrupted test message
+ assert(stdout.includes(`Interrupted while running: ${expectedTestName}`),
+ `Expected output to contain interrupted test name`);
assert.strictEqual(signal, null);
assert.strictEqual(code, 1);
}
@@ -67,6 +70,10 @@ if (process.argv[2] === 'child') {
assert.strictEqual(child.status, 1);
assert.strictEqual(child.signal, null);
- runAndKill(fixtures.path('test-runner', 'never_ending_sync.js')).then(common.mustCall());
- runAndKill(fixtures.path('test-runner', 'never_ending_async.js')).then(common.mustCall());
+ // With process isolation (default), the test name shown is the file path
+ // because the parent runner only knows about file-level tests
+ const neverEndingSync = fixtures.path('test-runner', 'never_ending_sync.js');
+ const neverEndingAsync = fixtures.path('test-runner', 'never_ending_async.js');
+ runAndKill(neverEndingSync, neverEndingSync).then(common.mustCall());
+ runAndKill(neverEndingAsync, neverEndingAsync).then(common.mustCall());
}
diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs
index 4fb0f0acb377c3..a5cac129ad1c21 100644
--- a/test/sequential/test-watch-mode.mjs
+++ b/test/sequential/test-watch-mode.mjs
@@ -893,4 +893,33 @@ process.on('message', (message) => {
await done();
}
});
+
+ it('should respect the order for --env-file and --env-file-if-exists', async () => {
+ const envKey = `TEST_ENV_${Date.now()}`;
+ const jsFile = createTmpFile(`console.log('ENV: ' + process.env.${envKey});`);
+
+ const envFile = createTmpFile(`${envKey}=base`, '.env');
+ const envFileIfExists = createTmpFile(`${envKey}=override`, '.env');
+
+ const { done, restart } = runInBackground({
+ args: [
+ '--watch',
+ `--env-file=${envFile}`,
+ `--env-file-if-exists=${envFileIfExists}`,
+ jsFile,
+ ],
+ });
+
+ try {
+ const { stdout, stderr } = await restart();
+
+ assert.strictEqual(stderr, '');
+ assert.deepStrictEqual(stdout, [
+ 'ENV: override',
+ `Completed running ${inspect(jsFile)}. Waiting for file changes before restarting...`,
+ ]);
+ } finally {
+ await done();
+ }
+ });
});