From 8df6332f8a240a89da234bc60c041fca2923ba4c Mon Sep 17 00:00:00 2001 From: Amol Yadav Date: Sat, 14 Feb 2026 20:41:22 +0530 Subject: [PATCH 1/3] http2: add http1Options for HTTP/1 fallback configuration PR-URL: https://github.com/nodejs/node/pull/61713 Fixes: https://github.com/nodejs/node/issues/59783 Reviewed-By: Robert Nagy Reviewed-By: Stephen Belanger Reviewed-By: Tim Perry --- doc/api/deprecations.md | 38 +++++++++++++++++ doc/api/http2.md | 41 +++++++++++++++++++ lib/internal/http2/core.js | 29 ++++++++----- ...ttp2-https-fallback-http-server-options.js | 11 ++++- 4 files changed, 107 insertions(+), 12 deletions(-) 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/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-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()); } From 65df9ad6438ca0c852a8f208361ae9507e072c9e Mon Sep 17 00:00:00 2001 From: Efe Date: Sat, 14 Feb 2026 20:06:30 +0100 Subject: [PATCH 3/3] watch: get flags from execArgv PR-URL: https://github.com/nodejs/node/pull/61779 Reviewed-By: Moshe Atlow Reviewed-By: Yagiz Nizipli Reviewed-By: Colin Ihrig Reviewed-By: Marco Ippolito --- lib/internal/main/watch_mode.js | 9 ++++----- test/sequential/test-watch-mode.mjs | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) 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/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(); + } + }); });