From 1dacb39673e4c4a22c4e19275202b8779e3f7447 Mon Sep 17 00:00:00 2001 From: greenhead Date: Mon, 17 Aug 2026 12:39:42 +0900 Subject: [PATCH 1/9] stream: use validateObject for zlib/iter params The kValidateObjectAllowArray flag matches the replaced check: arrays keep passing and the thrown error is unchanged. Signed-off-by: greenhead PR-URL: https://github.com/nodejs/node/pull/65015 Reviewed-By: James M Snell --- lib/internal/streams/iter/transform.js | 5 ++-- .../test-stream-iter-transform-params.js | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-stream-iter-transform-params.js diff --git a/lib/internal/streams/iter/transform.js b/lib/internal/streams/iter/transform.js index 583c6e9b192d..cb35906ded02 100644 --- a/lib/internal/streams/iter/transform.js +++ b/lib/internal/streams/iter/transform.js @@ -40,6 +40,7 @@ const { isArrayBufferView, isAnyArrayBuffer } = require('internal/util/types'); const { kValidatedTransform } = require('internal/streams/iter/types'); const { checkRangesOrGetDefault, + kValidateObjectAllowArray, validateFiniteNumber, validateObject, } = require('internal/validators'); @@ -106,9 +107,7 @@ function validateDictionary(dictionary) { function validateParams(params, maxParam, errClass) { if (params === undefined) return; - if (typeof params !== 'object' || params === null) { - throw new ERR_INVALID_ARG_TYPE('options.params', 'Object', params); - } + validateObject(params, 'options.params', kValidateObjectAllowArray); const keys = ObjectKeys(params); for (let i = 0; i < keys.length; i++) { const origKey = keys[i]; diff --git a/test/parallel/test-stream-iter-transform-params.js b/test/parallel/test-stream-iter-transform-params.js new file mode 100644 index 000000000000..aa029f9713c9 --- /dev/null +++ b/test/parallel/test-stream-iter-transform-params.js @@ -0,0 +1,30 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { from, pull, bytes } = require('stream/iter'); +const { compressBrotli, compressZstd } = require('zlib/iter'); + +// Type validation of options.params in zlib/iter transforms: plain +// objects and arrays pass the check, any other value rejects with +// ERR_INVALID_ARG_TYPE. Arrays have always passed the typeof-based +// check, so this behavior must be preserved by any refactor. + +const consume = (transform) => bytes(pull(from('test'), transform)); + +(async () => { + for (const compress of [compressBrotli, compressZstd]) { + for (const params of [42, 'bad', true, Symbol(), () => {}, null]) { + await assert.rejects( + consume(compress({ params })), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + } + + // An empty array has no own keys, so it passes both the type check + // and the per-key validation and compression succeeds. + const out = await consume(compress({ params: [] })); + assert.ok(out.byteLength > 0); + } +})().then(common.mustCall()); From 3abf65fc3edef8f28f7fe85adf2c2f23882f9167 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:01:47 -0700 Subject: [PATCH 2/9] sqlite: validate StatementSync.run() integers Use the standard SQLite integer conversion for changes and lastInsertRowid. Throw ERR_OUT_OF_RANGE when a value cannot be represented safely as a Number, or return it as a BigInt when BigInt reads are enabled. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: https://github.com/nodejs/node/pull/65178 Fixes: https://github.com/nodejs/node/issues/65177 Reviewed-By: Stephen Belanger --- src/node_sqlite.cc | 39 ++++++++++++--------- test/parallel/test-sqlite-statement-sync.js | 19 ++++++++++ 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 8e6a230f625a..d0da887062bd 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -105,6 +105,24 @@ void BindingData::CreatePerContextProperties(Local target, principal->AddBindingData(target); } +inline MaybeLocal IntegerToValue(Isolate* isolate, + sqlite3_int64 value, + bool use_big_ints) { + if (use_big_ints) { + return BigInt::New(isolate, value); + } + + if (value < -kMaxSafeJsInteger || value > kMaxSafeJsInteger) { + THROW_ERR_OUT_OF_RANGE( + isolate, + "Value is too large to be represented as a JavaScript number: %" PRId64, + value); + return MaybeLocal(); + } + + return Number::New(isolate, value); +} + #define CHECK_ERROR_OR_THROW(isolate, db, expr, expected, ret) \ do { \ int r_ = (expr); \ @@ -159,16 +177,7 @@ void BindingData::CreatePerContextProperties(Local target, switch (sqlite3_##from##_type(__VA_ARGS__)) { \ case SQLITE_INTEGER: { \ sqlite3_int64 val = sqlite3_##from##_int64(__VA_ARGS__); \ - if ((use_big_int_args)) { \ - (result) = BigInt::New((isolate), val); \ - } else if (std::abs(val) <= kMaxSafeJsInteger) { \ - (result) = Number::New((isolate), val); \ - } else { \ - THROW_ERR_OUT_OF_RANGE((isolate), \ - "Value is too large to be represented as a " \ - "JavaScript number: %" PRId64, \ - val); \ - } \ + (result) = IntegerToValue((isolate), val, (use_big_int_args)); \ break; \ } \ case SQLITE_FLOAT: { \ @@ -3251,12 +3260,10 @@ MaybeLocal StatementExecutionHelper::Run(Environment* env, Local last_insert_rowid_val; Local changes_val; - if (use_big_ints) { - last_insert_rowid_val = BigInt::New(isolate, last_insert_rowid); - changes_val = BigInt::New(isolate, changes); - } else { - last_insert_rowid_val = Number::New(isolate, last_insert_rowid); - changes_val = Number::New(isolate, changes); + if (!IntegerToValue(isolate, last_insert_rowid, use_big_ints) + .ToLocal(&last_insert_rowid_val) || + !IntegerToValue(isolate, changes, use_big_ints).ToLocal(&changes_val)) { + return MaybeLocal(); } auto run_result_template = env->sqlite_run_result_template(); diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js index b13da41e9d71..44eb482f9bdb 100644 --- a/test/parallel/test-sqlite-statement-sync.js +++ b/test/parallel/test-sqlite-statement-sync.js @@ -821,6 +821,25 @@ suite('StatementSync.prototype.setReadBigInts()', () => { }); }); + test('BigInt is required for reading large last insert row IDs', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY) STRICT'); + const insert = db.prepare('INSERT INTO data VALUES (?)'); + + t.assert.throws(() => { + insert.run(9007199254740993n); + }, { + code: 'ERR_OUT_OF_RANGE', + message: /^Value is too large to be represented as a JavaScript number: 9007199254740993$/, + }); + + insert.setReadBigInts(true); + t.assert.deepStrictEqual(insert.run(9007199254740995n), { + changes: 1n, + lastInsertRowid: 9007199254740995n, + }); + }); + test('throws if the statement is already finalized', (t) => { using db = new DatabaseSync(':memory:'); const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); From 977c20ed675e9c240e841ccf9edd06c5a4b239a4 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Sun, 16 Aug 2026 22:02:53 -0700 Subject: [PATCH 3/9] stream: avoid leaking consumers on signal failure Validate Broadcast.push() and Share.pull() signals before registering raw consumers. Return a rejecting iterable for pre-aborted signals without adding a cursor. This prevents failed subscriptions from leaving unreachable cursors that inflate consumerCount and can permanently impose backpressure. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: https://github.com/nodejs/node/pull/65299 Fixes: https://github.com/nodejs/node/issues/65298 Reviewed-By: James M Snell Reviewed-By: Jason Zhang --- lib/internal/streams/iter/broadcast.js | 21 +++++++++++++--- lib/internal/streams/iter/share.js | 24 ++++++++++++++++--- .../test-stream-iter-broadcast-basic.js | 12 ++++++++++ test/parallel/test-stream-iter-share-async.js | 12 ++++++++++ test/parallel/test-stream-iter-validation.js | 18 ++++++++++++++ 5 files changed, 81 insertions(+), 6 deletions(-) diff --git a/lib/internal/streams/iter/broadcast.js b/lib/internal/streams/iter/broadcast.js index a1384c9e4d51..4c63774bbb32 100644 --- a/lib/internal/streams/iter/broadcast.js +++ b/lib/internal/streams/iter/broadcast.js @@ -116,17 +116,32 @@ class BroadcastImpl { push(...args) { const { transforms, options } = parsePullArgs(args); + const signal = options?.signal; + validateAbortSignal(signal, 'options.signal'); + + // Avoid registering a consumer that the pre-aborted pipeline will never + // read or detach. + if (signal?.aborted) { + return { + __proto__: null, + // eslint-disable-next-line require-yield + async *[SymbolAsyncIterator]() { + throw signal.reason; + }, + }; + } + const rawConsumer = this.#createRawConsumer(); // When transforms are present, delegate to pull() which creates its // own internal AbortController that follows the external signal. // When no transforms, return rawConsumer directly (controller elided // per PULL-02 optimization -- no transforms means no signal recipient). - if (transforms.length > 0 || options?.signal) { + if (transforms.length > 0 || signal) { const pullArgs = [...transforms]; - if (options?.signal) { + if (signal) { ArrayPrototypePush(pullArgs, - { __proto__: null, signal: options.signal }); + { __proto__: null, signal }); } return pullWithTransforms(rawConsumer, ...pullArgs); } diff --git a/lib/internal/streams/iter/share.js b/lib/internal/streams/iter/share.js index 97efe7f3065b..662e57a7df55 100644 --- a/lib/internal/streams/iter/share.js +++ b/lib/internal/streams/iter/share.js @@ -93,11 +93,29 @@ class ShareImpl { pull(...args) { const { transforms, options } = parsePullArgs(args); + const signal = options?.signal; + validateAbortSignal(signal, 'options.signal'); + + // Avoid registering a consumer that the pre-aborted pipeline will never + // read or detach. + if (signal?.aborted) { + return { + __proto__: null, + // eslint-disable-next-line require-yield + async *[SymbolAsyncIterator]() { + throw signal.reason; + }, + }; + } + const rawConsumer = this.#createRawConsumer(); - if (transforms.length > 0 || options?.signal) { - if (options) { - return pullWithTransforms(rawConsumer, ...transforms, options); + if (transforms.length > 0 || signal) { + if (signal) { + return pullWithTransforms( + rawConsumer, + ...transforms, + { __proto__: null, signal }); } return pullWithTransforms(rawConsumer, ...transforms); } diff --git a/test/parallel/test-stream-iter-broadcast-basic.js b/test/parallel/test-stream-iter-broadcast-basic.js index 125f386210c9..4438e82a7817 100644 --- a/test/parallel/test-stream-iter-broadcast-basic.js +++ b/test/parallel/test-stream-iter-broadcast-basic.js @@ -203,6 +203,17 @@ async function testPushAbortSignalRejectsPendingNext() { await rejected; } +async function testPushPreAbortedSignalDoesNotAddConsumer() { + const reason = new Error('already aborted'); + const signal = AbortSignal.abort(reason); + const { broadcast: bc } = broadcast(); + const iter = bc.push({ signal })[Symbol.asyncIterator](); + + assert.strictEqual(bc.consumerCount, 0); + await assert.rejects(iter.next(), (error) => error === reason); + assert.strictEqual(bc.consumerCount, 0); +} + // ============================================================================= // Writer fail detaches consumers // ============================================================================= @@ -331,6 +342,7 @@ Promise.all([ testCancelWithFalsyReason(), testPendingNextSettlesAfterReturn(), testPushAbortSignalRejectsPendingNext(), + testPushPreAbortedSignalDoesNotAddConsumer(), testFailDetachesConsumers(), testWriterFailIdempotent(), testLateJoinerSeesBufferedData(), diff --git a/test/parallel/test-stream-iter-share-async.js b/test/parallel/test-stream-iter-share-async.js index 314e7bfcf01c..c96a0cb0f3c3 100644 --- a/test/parallel/test-stream-iter-share-async.js +++ b/test/parallel/test-stream-iter-share-async.js @@ -226,6 +226,17 @@ async function testSharePullAbortSignalRejectsPendingNext() { shared.cancel(); } +async function testSharePullPreAbortedSignalDoesNotAddConsumer() { + const reason = new Error('already aborted'); + const signal = AbortSignal.abort(reason); + const shared = share(from('data')); + const iter = shared.pull({ signal })[Symbol.asyncIterator](); + + assert.strictEqual(shared.consumerCount, 0); + await assert.rejects(iter.next(), (error) => error === reason); + assert.strictEqual(shared.consumerCount, 0); +} + async function testShareAlreadyAborted() { const shared = share(from('data'), { signal: AbortSignal.abort() }); const consumer = shared.pull(); @@ -372,6 +383,7 @@ Promise.all([ testShareAbortSignal(), testShareAbortSignalWhileSourcePullPending(), testSharePullAbortSignalRejectsPendingNext(), + testSharePullPreAbortedSignalDoesNotAddConsumer(), testShareAlreadyAborted(), testShareSourceError(), testShareLateJoiningConsumer(), diff --git a/test/parallel/test-stream-iter-validation.js b/test/parallel/test-stream-iter-validation.js index 9f77fe330478..8dcfb46f173f 100644 --- a/test/parallel/test-stream-iter-validation.js +++ b/test/parallel/test-stream-iter-validation.js @@ -156,6 +156,15 @@ assert.throws(() => broadcast({ budget: 16383 }), { code: 'ERR_OUT_OF_RANGE' }); assert.throws(() => broadcast({ signal: {} }), { code: 'ERR_INVALID_ARG_TYPE' }); assert.throws(() => broadcast({ backpressure: 'bad' }), { code: 'ERR_INVALID_ARG_VALUE' }); +// Broadcast consumer options.signal must be AbortSignal and validation must +// not leave a consumer registered. +{ + const { broadcast: bc } = broadcast(); + assert.throws(() => bc.push({ signal: {} }), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.strictEqual(bc.consumerCount, 0); +} + // BroadcastWriter options.signal must be AbortSignal { const { writer } = broadcast(); @@ -212,6 +221,15 @@ assert.throws(() => share(from('a'), { budget: Number.MAX_SAFE_INTEGER + 1 }), assert.throws(() => share(from('a'), { signal: {} }), { code: 'ERR_INVALID_ARG_TYPE' }); assert.throws(() => share(from('a'), { backpressure: 'bad' }), { code: 'ERR_INVALID_ARG_VALUE' }); +// Share consumer options.signal must be AbortSignal and validation must not +// leave a consumer registered. +{ + const shared = share(from('a')); + assert.throws(() => shared.pull({ signal: {} }), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.strictEqual(shared.consumerCount, 0); +} + // share() values < 16384 are rejected assert.throws(() => share(from('a'), { budget: 0 }), { code: 'ERR_OUT_OF_RANGE' }); assert.throws(() => share(from('a'), { budget: -1 }), { code: 'ERR_OUT_OF_RANGE' }); From 11176fdfbea90f9e5557a4f5c5b638d97f571cb1 Mon Sep 17 00:00:00 2001 From: Seongeun Lee Date: Mon, 17 Aug 2026 17:07:35 +0900 Subject: [PATCH 4/9] typings: add watchdog internal binding types Add a WatchdogBinding declaration for internalBinding('watchdog') and wire it into InternalBindingMap. Signed-off-by: leah-1ee PR-URL: https://github.com/nodejs/node/pull/65228 Reviewed-By: Daeyeon Jeong Reviewed-By: James M Snell --- typings/globals.d.ts | 2 ++ typings/internalBinding/watchdog.d.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 typings/internalBinding/watchdog.d.ts diff --git a/typings/globals.d.ts b/typings/globals.d.ts index b294042d790b..95b7b1356aef 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -35,6 +35,7 @@ import { URLPatternBinding } from "./internalBinding/url_pattern"; import { UtilBinding } from './internalBinding/util'; import { UVBinding } from './internalBinding/uv'; import { WASIBinding } from './internalBinding/wasi'; +import { WatchdogBinding } from './internalBinding/watchdog'; import { WorkerBinding } from './internalBinding/worker'; import { ZlibBinding } from './internalBinding/zlib'; @@ -76,6 +77,7 @@ interface InternalBindingMap { util: UtilBinding; uv: UVBinding; wasi: WASIBinding; + watchdog: WatchdogBinding; worker: WorkerBinding; zlib: ZlibBinding; } diff --git a/typings/internalBinding/watchdog.d.ts b/typings/internalBinding/watchdog.d.ts new file mode 100644 index 000000000000..917ee3b96d48 --- /dev/null +++ b/typings/internalBinding/watchdog.d.ts @@ -0,0 +1,15 @@ +declare namespace InternalWatchdogBinding { + class TraceSigintWatchdog { + constructor(); + start(): void; + stop(): void; + close(callback?: () => void): void; + hasRef(): boolean; + ref(): void; + unref(): void; + } +} + +export interface WatchdogBinding { + TraceSigintWatchdog: typeof InternalWatchdogBinding.TraceSigintWatchdog; +} From 671ff97a032797aae978c094ea7af87bb59da028 Mon Sep 17 00:00:00 2001 From: Seongeun Lee Date: Mon, 17 Aug 2026 17:07:48 +0900 Subject: [PATCH 5/9] typings: add diagnostics_channel typings Add a DiagnosticsChannelBinding declaration for internalBinding('diagnostics_channel') and wire it into InternalBindingMap. Signed-off-by: leah-1ee PR-URL: https://github.com/nodejs/node/pull/65227 Reviewed-By: Daeyeon Jeong Reviewed-By: James M Snell --- typings/globals.d.ts | 2 ++ typings/internalBinding/diagnostics_channel.d.ts | 6 ++++++ 2 files changed, 8 insertions(+) create mode 100644 typings/internalBinding/diagnostics_channel.d.ts diff --git a/typings/globals.d.ts b/typings/globals.d.ts index 95b7b1356aef..6f6ca5d3f936 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -8,6 +8,7 @@ import { ConstantsBinding } from './internalBinding/constants'; import { CredentialsBinding } from './internalBinding/credentials'; import { CryptoBinding } from './internalBinding/crypto'; import { DebugBinding } from './internalBinding/debug'; +import { DiagnosticsChannelBinding } from './internalBinding/diagnostics_channel'; import { EncodingBinding } from './internalBinding/encoding_binding'; import { FsBinding } from './internalBinding/fs'; import { FsDirBinding } from './internalBinding/fs_dir'; @@ -50,6 +51,7 @@ interface InternalBindingMap { credentials: CredentialsBinding; crypto: CryptoBinding; debug: DebugBinding; + diagnostics_channel: DiagnosticsChannelBinding; encoding_binding: EncodingBinding; fs: FsBinding; fs_dir: FsDirBinding; diff --git a/typings/internalBinding/diagnostics_channel.d.ts b/typings/internalBinding/diagnostics_channel.d.ts new file mode 100644 index 000000000000..e6297d45ace0 --- /dev/null +++ b/typings/internalBinding/diagnostics_channel.d.ts @@ -0,0 +1,6 @@ +export interface DiagnosticsChannelBinding { + subscribers: Uint32Array; + linkNativeChannel( + callback: (name: string, index: number) => object | undefined, + ): void; +} From 2004fbd1642f19fab4ad569bfcef4180d9ff536a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 10 Aug 2026 13:46:04 +0200 Subject: [PATCH 6/9] debugger: wait for target startup The inspector can accept a connection before an --inspect-brk target enters its frontend wait. Runtime.runIfWaitingForDebugger can then be handled too early, allowing the target to subsequently block forever. Wait for NodeRuntime.waitingForDebugger before initializing and releasing launched targets. Race the handshake against disconnects and apply it to both interactive and probe startup. Refs: https://github.com/nodejs/node/issues/64116 Assisted-by: codex:gpt-5.6-sol Co-authored-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/65194 Reviewed-By: Trivikram Kamat Reviewed-By: James M Snell --- lib/internal/debugger/inspect_helpers.js | 196 +++++++++++++++++- lib/internal/debugger/inspect_probe.js | 12 ++ lib/internal/debugger/inspect_repl.js | 7 +- test/parallel/test-debugger-no-inspect-brk.js | 158 ++++++++++++++ .../test-debugger-probe-startup-disconnect.js | 56 +++++ .../test-debugger-run-restart-init.js | 66 +++++- .../test-debugger-wait-for-debugger.js | 137 ++++++++++++ 7 files changed, 629 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-debugger-no-inspect-brk.js create mode 100644 test/parallel/test-debugger-probe-startup-disconnect.js create mode 100644 test/parallel/test-debugger-wait-for-debugger.js diff --git a/lib/internal/debugger/inspect_helpers.js b/lib/internal/debugger/inspect_helpers.js index f83876e96bc0..68d7901b38c5 100644 --- a/lib/internal/debugger/inspect_helpers.js +++ b/lib/internal/debugger/inspect_helpers.js @@ -1,11 +1,20 @@ 'use strict'; const { + ArrayPrototypePop, + ArrayPrototypePush, ArrayPrototypePushApply, + MapPrototypeGet, Number, Promise, + PromiseWithResolvers, RegExpPrototypeExec, + RegExpPrototypeSymbolReplace, + SafePromiseRace, StringPrototypeEndsWith, + StringPrototypeIndexOf, + StringPrototypeSlice, + StringPrototypeStartsWith, } = primordials; const { spawn } = require('child_process'); @@ -18,12 +27,24 @@ const { AbortController, } = require('internal/abort_controller'); -const { ERR_DEBUGGER_STARTUP_ERROR } = require('internal/errors').codes; +const { + ERR_DEBUGGER_ERROR, + ERR_DEBUGGER_STARTUP_ERROR, +} = require('internal/errors').codes; const { exitCodes: { kInvalidCommandLineArgument, }, } = internalBinding('errors'); +const { + types: { + kBoolean, + kNoOp, + kV8Option, + }, +} = internalBinding('options'); + +const { getCLIOptionsInfo } = require('internal/options'); const debugRegex = /Debugger listening on ws:\/\/\[?(.+?)\]?:(\d+)\//; @@ -61,6 +82,176 @@ function ensureTrailingNewline(text) { return StringPrototypeEndsWith(text, '\n') ? text : `${text}\n`; } +// Mirror OptionsParser::Parse() far enough to find the child script. Options +// before it must not undo the inspector setup added by launchChildProcess(). +function validateChildArgs(childArgs) { + const { options, aliases } = getCLIOptionsInfo(); + const syntheticArgs = []; + let breakFirstLine = true; + let childArgIndex = 0; + let inspectorEnabled = true; + + function peekArg() { + return syntheticArgs.length > 0 ? + syntheticArgs[syntheticArgs.length - 1] : + childArgs[childArgIndex]; + } + + function shiftArg() { + return syntheticArgs.length > 0 ? + ArrayPrototypePop(syntheticArgs) : + childArgs[childArgIndex++]; + } + + while (true) { + const nextArg = peekArg(); + if (nextArg === undefined || nextArg.length <= 1 || nextArg[0] !== '-') { + break; + } + + const isSynthetic = syntheticArgs.length > 0; + const arg = shiftArg(); + if (arg === '--') { break; } + if (!isSynthetic && + (arg === '--experimental-config-file' || + arg === '--experimental-default-config-file')) { + // ConfigReader rewrites these to an inline default path before parsing. + continue; + } + if (!isSynthetic && + StringPrototypeStartsWith( + arg, '--experimental-default-config-file=')) { + // ConfigReader rejects this form before parsing the remaining options. + return; + } + + const equalsIndex = arg[1] === '-' ? StringPrototypeIndexOf(arg, '=') : -1; + let name = equalsIndex === -1 ? arg : StringPrototypeSlice(arg, 0, equalsIndex); + if (name.length > 2) { + name = `${StringPrototypeSlice(name, 0, 2)}${ + RegExpPrototypeSymbolReplace(/_/g, StringPrototypeSlice(name, 2), '-')}`; + } + + let isNegation = false; + if (StringPrototypeStartsWith(name, '--no-')) { + name = `--${StringPrototypeSlice(name, 5)}`; + isNegation = true; + } + + while (true) { + let expansion = MapPrototypeGet(aliases, name); + if (expansion === undefined && equalsIndex !== -1) { + expansion = MapPrototypeGet(aliases, `${name}=`); + } + const aliasArg = peekArg(); + if (expansion === undefined && + aliasArg !== undefined && + aliasArg.length > 0 && + aliasArg[0] !== '-') { + expansion = MapPrototypeGet(aliases, `${name} `); + } + if (expansion === undefined) { break; } + + const previousName = name; + // process.allowedNodeEnvironmentFlags may remove a self-recursive + // first entry from the cached alias metadata. Preserve the native + // parser's synthetic option terminator in that case. + if (expansion[0] === '--') { + for (let i = expansion.length - 1; i >= 0; i--) { + ArrayPrototypePush(syntheticArgs, expansion[i]); + } + break; + } + name = expansion[0]; + for (let i = expansion.length - 1; i > 0; i--) { + ArrayPrototypePush(syntheticArgs, expansion[i]); + } + if (name === previousName) { break; } + } + + const info = MapPrototypeGet(options, name); + if (info === undefined) { continue; } + if (isNegation && info.type !== kBoolean && info.type !== kV8Option) { + return; + } + if (info.type === kBoolean || info.type === kNoOp || info.type === kV8Option) { + if (name === '--inspect') { + inspectorEnabled = !isNegation; + } else if (name === '--inspect-brk') { + breakFirstLine = !isNegation; + if (!isNegation) { inspectorEnabled = true; } + } else if (!isNegation && + (name === '--inspect-wait' || + name === '--inspect-brk-node')) { + inspectorEnabled = true; + } + continue; + } + + if (equalsIndex !== -1) { + if (equalsIndex === arg.length - 1) { return; } + continue; + } + + const value = peekArg(); + if (value === undefined || (value.length > 0 && value[0] === '-')) { + return; + } + shiftArg(); + } + + if (!inspectorEnabled) { + throw new ERR_DEBUGGER_STARTUP_ERROR( + '--no-inspect is incompatible with node inspect before the child script'); + } + if (!breakFirstLine) { + throw new ERR_DEBUGGER_STARTUP_ERROR( + '--no-inspect-brk is incompatible with node inspect before the child script'); + } +} + +async function waitForDebugger( + client, + callMethod = (method) => client.callMethod(method), +) { + const { + promise: waitingPromise, + resolve: resolveWaiting, + } = PromiseWithResolvers(); + const { + promise: closedPromise, + reject: rejectClosed, + } = PromiseWithResolvers(); + const onWaiting = () => resolveWaiting(); + const onClose = () => { + rejectClosed(new ERR_DEBUGGER_ERROR( + 'Debugger session ended while waiting for target startup')); + }; + + // The inspector can accept a connection before the target reaches its + // startup wait. Enabling NodeRuntime makes that state observable whether + // the target was already waiting or starts waiting later. + client.once('NodeRuntime.waitingForDebugger', onWaiting); + client.once('close', onClose); + try { + await SafePromiseRace([ + callMethod('NodeRuntime.enable'), + closedPromise, + ]); + await SafePromiseRace([ + waitingPromise, + closedPromise, + ]); + await SafePromiseRace([ + callMethod('NodeRuntime.disable'), + closedPromise, + ]); + } finally { + client.removeListener('NodeRuntime.waitingForDebugger', onWaiting); + client.removeListener('close', onClose); + } +} + function writeInspectUsageAndExit(invokedAs, message, exitCode) { const code = exitCode ?? (message ? kInvalidCommandLineArgument : 0); const out = code === 0 ? process.stdout : process.stderr; @@ -141,6 +332,8 @@ probe output schema. async function launchChildProcess(childArgs, inspectHost, inspectPort, childOutput, options = { __proto__: null }) { + validateChildArgs(childArgs); + if (!options.skipPortPreflight) { await portIsFree(inspectHost, inspectPort); } @@ -189,5 +382,6 @@ async function launchChildProcess(childArgs, inspectHost, inspectPort, module.exports = { ensureTrailingNewline, launchChildProcess, + waitForDebugger, writeInspectUsageAndExit, }; diff --git a/lib/internal/debugger/inspect_probe.js b/lib/internal/debugger/inspect_probe.js index b6cac6dc5779..3b77a46f1ba1 100644 --- a/lib/internal/debugger/inspect_probe.js +++ b/lib/internal/debugger/inspect_probe.js @@ -33,6 +33,7 @@ const InspectClient = require('internal/debugger/inspect_client'); const { ensureTrailingNewline, launchChildProcess, + waitForDebugger, } = require('internal/debugger/inspect_helpers'); const { ERR_DEBUGGER_STARTUP_ERROR } = require('internal/errors').codes; @@ -1044,6 +1045,17 @@ class ProbeInspectorSession { this.connected = true; try { + try { + await waitForDebugger( + this.client, + (method) => this.callCdp(method), + ); + } catch (err) { + // A close event may have completed the structured report while the + // readiness helper was rejecting its disconnect race. + if (this.finished) { throw kInspectorFailedSentinel; } + throw err; + } await this.callCdp('Runtime.enable'); await this.callCdp('Debugger.enable'); await this.bindBreakpoints(); diff --git a/lib/internal/debugger/inspect_repl.js b/lib/internal/debugger/inspect_repl.js index 548df089fb14..69ca174dd241 100644 --- a/lib/internal/debugger/inspect_repl.js +++ b/lib/internal/debugger/inspect_repl.js @@ -60,6 +60,7 @@ const { fileURLToPath } = require('internal/url'); const { customInspectSymbol, SideEffectFreeRegExpPrototypeSymbolReplace } = require('internal/util'); const { inspect: utilInspect } = require('internal/util/inspect'); const { isObjectLiteral } = require('internal/repl/utils'); +const { waitForDebugger } = require('internal/debugger/inspect_helpers'); const debuglog = require('internal/util/debuglog').debuglog('inspect'); const SHORTCUTS = { @@ -1204,9 +1205,13 @@ function createRepl(inspector) { } async function initAfterStart() { + const waitForDebuggerOnStart = !!inspector.options?.script; waitForInitialBreakRender = - !!inspector.options?.script && + waitForDebuggerOnStart && process.env.NODE_INSPECT_RESUME_ON_START !== '1'; + if (waitForDebuggerOnStart) { + await waitForDebugger(inspector.client); + } await Runtime.enable(); await Profiler.enable(); await Profiler.setSamplingInterval({ interval: 100 }); diff --git a/test/parallel/test-debugger-no-inspect-brk.js b/test/parallel/test-debugger-no-inspect-brk.js new file mode 100644 index 000000000000..611d28fd9f3e --- /dev/null +++ b/test/parallel/test-debugger-no-inspect-brk.js @@ -0,0 +1,158 @@ +// Flags: --expose-internals + +// This tests that child --no-inspect and --no-inspect-brk options cannot leave +// the inspector setup disabled, while remaining valid as application args. +'use strict'; + +const common = require('../common'); +common.skipIfInspectorDisabled(); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const { + spawnSyncAndAssert, + spawnSyncAndExit, +} = require('../common/child_process'); +const { assertProbeJson } = require('../common/debugger-probe'); +const { launchChildProcess } = require('internal/debugger/inspect_helpers'); + +const cwd = fixtures.path('debugger'); +const probeUrl = fixtures.fileURL('debugger', 'probe.js').href; +const probeArgs = [ + '--probe', 'probe.js:12', + '--expr', 'finalValue', +]; +const incompatibleInspectBrk = + /--no-inspect-brk is incompatible with node inspect before the child script/; +const incompatibleInspect = + /--no-inspect is incompatible with node inspect before the child script/; + +function assertSuccessfulProbe(childArgs) { + spawnSyncAndAssert(process.execPath, [ + 'inspect', + '--json', + ...probeArgs, + '--', + ...childArgs, + ], { cwd }, { + stdout(output) { + assertProbeJson(output, { + v: 2, + probes: [{ + expr: 'finalValue', + target: { suffix: 'probe.js', line: 12 }, + }], + results: [{ + probe: 0, + event: 'hit', + hit: 1, + location: { url: probeUrl, line: 12, column: 1 }, + result: { type: 'number', value: 81, description: '81' }, + }, { + event: 'completed', + }], + }); + }, + trim: true, + }); +} + +for (const childOptions of [ + ['--require', 'assert', '--no-inspect-brk'], + ['--require=assert', '--no-inspect-brk'], + ['-r', 'assert', '--no_inspect_brk'], +]) { + spawnSyncAndExit(process.execPath, [ + 'inspect', + ...probeArgs, + '--', + ...childOptions, + 'probe.js', + ], { cwd }, { + signal: null, + status: 1, + stderr: incompatibleInspectBrk, + trim: true, + }); +} + +spawnSyncAndExit(process.execPath, [ + 'inspect', + ...probeArgs, + '--', + '--require', 'assert', + '--no-inspect', + 'probe.js', +], { cwd }, { + signal: null, + status: 1, + stderr: incompatibleInspect, + trim: true, +}); + +for (const { option, error } of [ + { option: '--no-inspect-brk', error: incompatibleInspectBrk }, + { option: '--no-inspect', error: incompatibleInspect }, +]) { + spawnSyncAndExit(process.execPath, [ + 'inspect', + option, + 'probe.js', + ], { cwd }, { + signal: null, + status: 1, + stderr: error, + trim: true, + }); + + assertSuccessfulProbe(['probe.js', option]); +} + +// Node options are last-write-wins. A later --inspect-brk restores both +// startup requirements. +assertSuccessfulProbe([ + '--no-inspect', + '--no-inspect-brk', + '--inspect-brk', + 'probe.js', +]); + +// ConfigReader rewrites these bare options to use the default path without +// consuming the following argument. +Promise.all([ + assert.rejects( + launchChildProcess([ + '--experimental-config-file', + '--no-inspect-brk', + 'probe.js', + ], '127.0.0.1', 0, () => {}), + incompatibleInspectBrk, + ), + assert.rejects( + launchChildProcess([ + '--experimental-default-config-file', + '--no-inspect-brk', + 'probe.js', + ], '127.0.0.1', 0, () => {}), + incompatibleInspectBrk, + ), + // These options imply --inspect, but do not restore --inspect-brk. + assert.rejects( + launchChildProcess([ + '--no-inspect', + '--inspect-wait', + '--no-inspect-brk', + 'probe.js', + ], '127.0.0.1', 0, () => {}), + incompatibleInspectBrk, + ), + assert.rejects( + launchChildProcess([ + '--no-inspect', + '--inspect-brk-node', + '--no-inspect-brk', + 'probe.js', + ], '127.0.0.1', 0, () => {}), + incompatibleInspectBrk, + ), +]).then(common.mustCall()); diff --git a/test/parallel/test-debugger-probe-startup-disconnect.js b/test/parallel/test-debugger-probe-startup-disconnect.js new file mode 100644 index 000000000000..68c8432dc552 --- /dev/null +++ b/test/parallel/test-debugger-probe-startup-disconnect.js @@ -0,0 +1,56 @@ +// Flags: --expose-internals +// This tests that a disconnect while probe mode is waiting for target startup +// is reported as a structured probe failure instead of an internal error. +'use strict'; + +const common = require('../common'); +common.skipIfInspectorDisabled(); + +const assert = require('assert'); +const { EventEmitter } = require('events'); +const { assertProbeJson } = require('../common/debugger-probe'); +const { ProbeInspectorSession } = require('internal/debugger/inspect_probe'); + +const probe = { + expr: 'value', + target: { suffix: 'probe-target.js', line: 1 }, +}; +const client = new EventEmitter(); +client.connect = common.mustCall(); +client.callMethod = common.mustCall((method) => { + assert.strictEqual(method, 'NodeRuntime.enable'); + setImmediate(() => client.emit('close')); + return new Promise(() => {}); +}); +client.reset = common.mustCall(); + +const session = new ProbeInspectorSession({ + childArgv: ['-e', ''], + host: '127.0.0.1', + port: 0, + probes: [probe], + skipPortPreflight: true, +}); +session.client = client; + +session.run().then(common.mustCall(({ code, report }) => { + assert.strictEqual(code, 1); + assertProbeJson(report, { + v: 2, + probes: [probe], + results: [{ + event: 'error', + pending: [0], + error: { + code: 'probe_failure', + message: + 'Inspector connection lost before probes started before probes: ' + + 'probe-target.js:1. The target startup may have torn down the ' + + 'inspector. If startup does not touch the inspector, this is likely ' + + 'a Node.js bug. Please file an issue.', + stderr: '', + details: { lastCdpMethod: 'NodeRuntime.enable' }, + }, + }], + }); +})); diff --git a/test/parallel/test-debugger-run-restart-init.js b/test/parallel/test-debugger-run-restart-init.js index 78f237353baf..b57939135f80 100644 --- a/test/parallel/test-debugger-run-restart-init.js +++ b/test/parallel/test-debugger-run-restart-init.js @@ -79,9 +79,29 @@ async function assertCommandWaitsForInit(repl, command, gate, calls) { const runGate = createGate(); const restartGate = createGate(); const gates = [null, runGate, restartGate]; + const client = new EventEmitter(); + let nodeRuntimeEnableCount = 0; + client.callMethod = common.mustCall(async (method) => { + calls.push(method); + if (method === 'NodeRuntime.enable') { + const emitWaiting = () => { + calls.push('NodeRuntime.waitingForDebugger'); + client.emit('NodeRuntime.waitingForDebugger'); + }; + // Cover notifications arriving both before and after the enable reply. + if (nodeRuntimeEnableCount++ % 2 === 0) { + emitWaiting(); + } else { + setImmediate(emitWaiting); + } + } else { + assert.strictEqual(method, 'NodeRuntime.disable'); + } + }, 6); const inspector = { - client: new EventEmitter(), + client, domainNames: ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime'], + options: { script: 'debugger-target.js' }, stdin: new PassThrough(), stdout: new PassThrough(), run: common.mustCall(async () => { @@ -101,6 +121,29 @@ async function assertCommandWaitsForInit(repl, command, gate, calls) { await assertCommandWaitsForInit(repl, 'run', runGate, calls); await assertCommandWaitsForInit(repl, 'restart', restartGate, calls); + assert.deepStrictEqual( + calls.filter((call) => ( + call === 'NodeRuntime.enable' || + call === 'NodeRuntime.waitingForDebugger' || + call === 'NodeRuntime.disable' || + call === 'Runtime.runIfWaitingForDebugger' + )), + [ + 'NodeRuntime.enable', + 'NodeRuntime.waitingForDebugger', + 'NodeRuntime.disable', + 'Runtime.runIfWaitingForDebugger', + 'NodeRuntime.enable', + 'NodeRuntime.waitingForDebugger', + 'NodeRuntime.disable', + 'Runtime.runIfWaitingForDebugger', + 'NodeRuntime.enable', + 'NodeRuntime.waitingForDebugger', + 'NodeRuntime.disable', + 'Runtime.runIfWaitingForDebugger', + ], + ); + assert.deepStrictEqual( calls.filter((call) => ( call === 'inspector.run' || @@ -116,4 +159,25 @@ async function assertCommandWaitsForInit(repl, command, gate, calls) { ); repl.close(); + + const attachCalls = []; + const attachClient = new EventEmitter(); + attachClient.callMethod = common.mustNotCall(); + const attachInspector = { + client: attachClient, + domainNames: ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime'], + options: {}, + stdin: new PassThrough(), + stdout: new PassThrough(), + suspendReplWhile(fn) { + return fn(); + }, + }; + + for (const domain of attachInspector.domainNames) { + attachInspector[domain] = createAgent(domain, attachCalls, []); + } + + const attachRepl = await createRepl(attachInspector)(); + attachRepl.close(); })().then(common.mustCall()); diff --git a/test/parallel/test-debugger-wait-for-debugger.js b/test/parallel/test-debugger-wait-for-debugger.js new file mode 100644 index 000000000000..b438147ea832 --- /dev/null +++ b/test/parallel/test-debugger-wait-for-debugger.js @@ -0,0 +1,137 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const assert = require('assert'); +const { EventEmitter } = require('events'); +const { + waitForDebugger, +} = require('internal/debugger/inspect_helpers'); + +function assertListenersRemoved(client) { + assert.strictEqual( + client.listenerCount('NodeRuntime.waitingForDebugger'), + 0, + ); + assert.strictEqual(client.listenerCount('close'), 0); +} + +async function testWaitingNotification(beforeEnableReply) { + const client = new EventEmitter(); + const calls = []; + client.callMethod = common.mustCall(async (method) => { + calls.push(method); + const emitWaiting = () => { + client.emit('NodeRuntime.waitingForDebugger'); + }; + if (method === 'NodeRuntime.enable') { + if (beforeEnableReply) { + emitWaiting(); + } else { + setImmediate(emitWaiting); + } + } else { + assert.strictEqual(method, 'NodeRuntime.disable'); + } + }, 2); + + await waitForDebugger(client); + assert.deepStrictEqual(calls, [ + 'NodeRuntime.enable', + 'NodeRuntime.disable', + ]); + assertListenersRemoved(client); +} + +async function testCloseWhileWaiting(beforeEnableReply) { + const client = new EventEmitter(); + client.callMethod = common.mustCall((method) => { + assert.strictEqual(method, 'NodeRuntime.enable'); + setImmediate(() => client.emit('close')); + return beforeEnableReply ? new Promise(() => {}) : Promise.resolve(); + }); + + await assert.rejects( + waitForDebugger(client), + { + code: 'ERR_DEBUGGER_ERROR', + message: 'Debugger session ended while waiting for target startup', + }, + ); + assertListenersRemoved(client); +} + +async function testCloseWhileDisabling() { + const client = new EventEmitter(); + client.callMethod = common.mustCall((method) => { + if (method === 'NodeRuntime.enable') { + client.emit('NodeRuntime.waitingForDebugger'); + return Promise.resolve(); + } + assert.strictEqual(method, 'NodeRuntime.disable'); + setImmediate(() => client.emit('close')); + return new Promise(() => {}); + }, 2); + + await assert.rejects( + waitForDebugger(client), + { + code: 'ERR_DEBUGGER_ERROR', + message: 'Debugger session ended while waiting for target startup', + }, + ); + assertListenersRemoved(client); +} + +async function testEnableFailure() { + const client = new EventEmitter(); + const expected = new Error('NodeRuntime.enable failed'); + client.callMethod = common.mustCall(async (method) => { + assert.strictEqual(method, 'NodeRuntime.enable'); + throw expected; + }); + + await assert.rejects( + waitForDebugger(client), + (error) => { + assert.strictEqual(error, expected); + return true; + }, + ); + assertListenersRemoved(client); +} + +async function testDisableFailure() { + const client = new EventEmitter(); + const expected = new Error('NodeRuntime.disable failed'); + client.callMethod = common.mustCall(async (method) => { + if (method === 'NodeRuntime.enable') { + client.emit('NodeRuntime.waitingForDebugger'); + return; + } + assert.strictEqual(method, 'NodeRuntime.disable'); + throw expected; + }, 2); + + await assert.rejects( + waitForDebugger(client), + (error) => { + assert.strictEqual(error, expected); + return true; + }, + ); + assertListenersRemoved(client); +} + +(async () => { + await testWaitingNotification(true); + await testWaitingNotification(false); + await testCloseWhileWaiting(true); + await testCloseWhileWaiting(false); + await testCloseWhileDisabling(); + await testEnableFailure(); + await testDisableFailure(); +})().then(common.mustCall()); From 0795d827ae9ab2d8d9aa1c9393ac7848998eb8db Mon Sep 17 00:00:00 2001 From: Seongeun Lee Date: Mon, 17 Aug 2026 17:23:30 +0900 Subject: [PATCH 7/9] typings: add signal_wrap internal binding types Add a SignalWrapBinding declaration for internalBinding('signal_wrap') and wire it into InternalBindingMap. Signed-off-by: leah-1ee PR-URL: https://github.com/nodejs/node/pull/65229 Reviewed-By: James M Snell Reviewed-By: Daeyeon Jeong --- typings/globals.d.ts | 2 ++ typings/internalBinding/signal_wrap.d.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 typings/internalBinding/signal_wrap.d.ts diff --git a/typings/globals.d.ts b/typings/globals.d.ts index 6f6ca5d3f936..e64b8e6d89fc 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -27,6 +27,7 @@ import { ProcessBinding } from './internalBinding/process'; import { ProcessWrapBinding } from './internalBinding/process_wrap'; import { SeaBinding } from './internalBinding/sea'; import { SerdesBinding } from './internalBinding/serdes'; +import { SignalWrapBinding } from './internalBinding/signal_wrap'; import { StringDecoderBinding } from './internalBinding/string_decoder'; import { SymbolsBinding } from './internalBinding/symbols'; import { TimersBinding } from './internalBinding/timers'; @@ -70,6 +71,7 @@ interface InternalBindingMap { process_wrap: ProcessWrapBinding; sea: SeaBinding; serdes: SerdesBinding; + signal_wrap: SignalWrapBinding; string_decoder: StringDecoderBinding; symbols: SymbolsBinding; timers: TimersBinding; diff --git a/typings/internalBinding/signal_wrap.d.ts b/typings/internalBinding/signal_wrap.d.ts new file mode 100644 index 000000000000..4c6473cbee22 --- /dev/null +++ b/typings/internalBinding/signal_wrap.d.ts @@ -0,0 +1,16 @@ +declare namespace InternalSignalWrapBinding { + class Signal { + constructor(); + onsignal?: (signum: number) => void; + start(signum: number): number | undefined; + stop(): number; + close(callback?: () => void): void; + hasRef(): boolean; + ref(): void; + unref(): void; + } +} + +export interface SignalWrapBinding { + Signal: typeof InternalSignalWrapBinding.Signal; +} From cc60845420c091ce933f5efa2afc1eba4f7dea4b Mon Sep 17 00:00:00 2001 From: Ayoub Mabrouk <77799760+Ayoub-Mabrouk@users.noreply.github.com> Date: Mon, 17 Aug 2026 09:44:15 +0100 Subject: [PATCH 8/9] util: use more primordials in `comparisons.js` PR-URL: https://github.com/nodejs/node/pull/61198 Reviewed-By: Aviv Keller Reviewed-By: Jordan Harband --- lib/internal/util/comparisons.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js index 2582217febc6..6b608a66733a 100644 --- a/lib/internal/util/comparisons.js +++ b/lib/internal/util/comparisons.js @@ -761,7 +761,7 @@ function setEquiv(a, b, mode, memo) { // If the specified value doesn't exist in the second set it's a object // (or in loose mode: a non-matching primitive). Find the // deep-(mode-)equal element in a set copy to reduce duplicate checks. - array.push(val); + ArrayPrototypePush(array, val); } } @@ -891,7 +891,7 @@ function mapEquiv(a, b, mode, memo) { } array = []; } - array.push(key2); + ArrayPrototypePush(array, key2); } else { // By directly retrieving the value we prevent another b.has(key2) check in // almost all possible cases. @@ -907,7 +907,7 @@ function mapEquiv(a, b, mode, memo) { if (array === undefined) { array = []; } - array.push(key2); + ArrayPrototypePush(array, key2); } } } From 3781ebc149d58cf4604403f48c6109b6a9833b22 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 17 Aug 2026 15:03:06 +0200 Subject: [PATCH 9/9] test: enforce exit code in `test-http-server-stale-close` Signed-off-by: Antoine du Hamel PR-URL: https://github.com/nodejs/node/pull/65198 Reviewed-By: Luigi Pinca Reviewed-By: Colin Ihrig --- test/parallel/test-http-server-stale-close.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/parallel/test-http-server-stale-close.js b/test/parallel/test-http-server-stale-close.js index d33c756a4a30..909d02c920eb 100644 --- a/test/parallel/test-http-server-stale-close.js +++ b/test/parallel/test-http-server-stale-close.js @@ -35,7 +35,7 @@ if (process.env.NODE_TEST_FORK_PORT) { req.write('BAM'); req.end(); } else { - const server = http.createServer(common.mustCallAtLeast((req, res) => { + const server = http.createServer(common.mustCall((req, res) => { res.writeHead(200, { 'Content-Length': '42' }); req.pipe(res); assert.strictEqual(req.destroyed, false); @@ -45,9 +45,13 @@ if (process.env.NODE_TEST_FORK_PORT) { res.end(); })); })); - server.listen(0, function() { - fork(__filename, { + server.listen(0, common.mustCall(function() { + const cp = fork(__filename, { + stdio: 'inherit', env: { ...process.env, NODE_TEST_FORK_PORT: this.address().port } }); - }); + cp.once('exit', common.mustCall((code) => { + assert.strictEqual(code, 0); + })); + })); }