From da953863826fd072c32f54921fb80f605f7b53b4 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 17 Jul 2026 17:04:35 -0700 Subject: [PATCH] node: per-stream console inspectOptions, connect error Local suffix, v8 flag validation Three independent Node v26.3.0 compatibility gaps, each with the upstream test that covers it, copied verbatim. net: ExceptionWithHostPort dropped Node's fifth `additional` argument, which appends ` - Local (address:port)` to the message. Both connect-failure paths in net.ts already computed that string and threw it away, so a failed connect with a bound local address reported `connect ECONNREFUSED 127.0.0.1:12399` where Node reports `connect ECONNREFUSED 127.0.0.1:12399 - Local (127.0.0.1:12400)`. console: `new Console({ inspectOptions })` did not accept a Map keyed by stream, so per-stream options were read as a plain options object with no `colors` and silently ignored. Look the options up per stream instead, keeping the plain-object form applying to both streams. Uses $get and avoids constructing a Map so a tampered global Map cannot influence the result. v8: setFlagsFromString threw the not-implemented error before validating its argument. Validate first, so a non-string is rejected with ERR_INVALID_ARG_TYPE as in Node; a real flag string still reports the gap rather than pretending the flag was applied. Adds test-net-connect-local-error, test-console-tty-colors-per-stream and test-v8-flag-type-check. Verified against the Node v26.3.0 binary: console inspect output, the connect error message, and the v8 validation errors are identical across ten checks including per-stream colors, a Map missing an entry for the stream, and the colorMode conflict in both forms. --- src/js/builtins/ConsoleObject.ts | 17 +++++-- src/js/internal/shared.ts | 5 +- src/js/node/net.ts | 4 +- src/js/node/v8.ts | 6 ++- .../test-console-tty-colors-per-stream.js | 23 ++++++++++ .../test/parallel/test-v8-flag-type-check.js | 16 +++++++ .../test-net-connect-local-error.js | 46 +++++++++++++++++++ 7 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 test/js/node/test/parallel/test-console-tty-colors-per-stream.js create mode 100644 test/js/node/test/parallel/test-v8-flag-type-check.js create mode 100644 test/js/node/test/sequential/test-net-connect-local-error.js diff --git a/src/js/builtins/ConsoleObject.ts b/src/js/builtins/ConsoleObject.ts index a69076257668..a7edc4c4d7dc 100644 --- a/src/js/builtins/ConsoleObject.ts +++ b/src/js/builtins/ConsoleObject.ts @@ -304,10 +304,16 @@ export function createConsoleConstructor(console: typeof globalThis.console) { if (inspectOptions !== undefined) { validateObject(inspectOptions, "options.inspectOptions"); - if (inspectOptions.colors !== undefined && options.colorMode !== undefined) { - throw $ERR_INCOMPATIBLE_OPTION_PAIR( - 'Option "options.inspectOptions.color" cannot be used in combination with option "colorMode"', - ); + // inspectOptions may be a Map keyed by stream, giving each stream its own + // options; a plain object applies to both. + const isPerStream = $isMap(inspectOptions); + for (const stream of [stdout, stderr]) { + const perStreamOptions = isPerStream ? inspectOptions.$get(stream) : inspectOptions; + if (perStreamOptions?.colors !== undefined && options.colorMode !== undefined) { + throw $ERR_INCOMPATIBLE_OPTION_PAIR( + 'Option "options.inspectOptions.color" cannot be used in combination with option "colorMode"', + ); + } } optionsMap.set(this, inspectOptions); } @@ -481,7 +487,8 @@ export function createConsoleConstructor(console: typeof globalThis.console) { } } - const options = optionsMap.get(this); + const inspectOptions = optionsMap.get(this); + const options = $isMap(inspectOptions) ? inspectOptions.$get(stream) : inspectOptions; if (options) { if (options.colors === undefined) { options.colors = color; diff --git a/src/js/internal/shared.ts b/src/js/internal/shared.ts index 749d7eb4dff2..f1de18722da9 100644 --- a/src/js/internal/shared.ts +++ b/src/js/internal/shared.ts @@ -57,7 +57,7 @@ class ExceptionWithHostPort extends Error { port?: number; address: string; - constructor(err: number, syscall: string, address: string, port?: number) { + constructor(err: number, syscall: string, address: string, port?: number, additional?: string) { // TODO(joyeecheung): We have to use the type-checked // getSystemErrorName(err) to guard against invalid arguments from users. // This can be replaced with [ code ] = errmap.get(err) when this method @@ -70,6 +70,9 @@ class ExceptionWithHostPort extends Error { } else if (address) { details = ` ${address}`; } + if (additional) { + details += ` - Local (${additional})`; + } super(`${syscall} ${code}${details}`); diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 890dcec10900..2046ed40ae41 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -3135,7 +3135,7 @@ function afterConnect(status, handle, req, readable, writable) { if (localAddress && (localPort = req.localPort)) { details = localAddress + ":" + localPort; } - const ex = new ExceptionWithHostPort(status, "connect", req.address, req.port); + const ex = new ExceptionWithHostPort(status, "connect", req.address, req.port, details); if (details) { ex.localAddress = req.localAddress; ex.localPort = req.localPort; @@ -3197,7 +3197,7 @@ function createConnectionError(req, status) { details = localAddress + ":" + localPort; } - const ex = new ExceptionWithHostPort(status, "connect", req.address, req.port); + const ex = new ExceptionWithHostPort(status, "connect", req.address, req.port, details); if (details) { ex.localAddress = req.localAddress; ex.localPort = req.localPort; diff --git a/src/js/node/v8.ts b/src/js/node/v8.ts index 1ca1d06e0b0f..3ce53107d463 100644 --- a/src/js/node/v8.ts +++ b/src/js/node/v8.ts @@ -2,6 +2,7 @@ // This is a stub! None of this is actually implemented yet. const { hideFromStack, throwNotImplemented } = require("internal/shared"); +const { validateString } = require("internal/validators"); const jsc: typeof import("bun:jsc") = require("bun:jsc"); function notimpl(message) { @@ -91,7 +92,10 @@ function getHeapSpaceStatistics() { function getHeapCodeStatistics() { notimpl("getHeapCodeStatistics"); } -function setFlagsFromString() { +function setFlagsFromString(flags) { + // Validate before reporting the gap: node rejects a non-string argument + // regardless of whether the flag itself can be applied. + validateString(flags, "flags"); notimpl("setFlagsFromString"); } function deserialize(value) { diff --git a/test/js/node/test/parallel/test-console-tty-colors-per-stream.js b/test/js/node/test/parallel/test-console-tty-colors-per-stream.js new file mode 100644 index 000000000000..4831c1d2ef76 --- /dev/null +++ b/test/js/node/test/parallel/test-console-tty-colors-per-stream.js @@ -0,0 +1,23 @@ +'use strict'; +require('../common'); +const { Console } = require('console'); +const { PassThrough } = require('stream'); +const assert = require('assert'); + +const stdout = new PassThrough().setEncoding('utf8'); +const stderr = new PassThrough().setEncoding('utf8'); + +const console = new Console({ + stdout, + stderr, + inspectOptions: new Map([ + [stdout, { colors: true }], + [stderr, { colors: false }], + ]), +}); + +console.log('Hello', 42); +console.warn('Hello', 42); + +assert.strictEqual(stdout.read(), 'Hello \x1B[33m42\x1B[39m\n'); +assert.strictEqual(stderr.read(), 'Hello 42\n'); diff --git a/test/js/node/test/parallel/test-v8-flag-type-check.js b/test/js/node/test/parallel/test-v8-flag-type-check.js new file mode 100644 index 000000000000..e46c09b79725 --- /dev/null +++ b/test/js/node/test/parallel/test-v8-flag-type-check.js @@ -0,0 +1,16 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const v8 = require('v8'); + +[1, undefined].forEach((value) => { + assert.throws( + () => v8.setFlagsFromString(value), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "flags" argument must be of type string.' + + common.invalidArgTypeHelper(value) + } + ); +}); diff --git a/test/js/node/test/sequential/test-net-connect-local-error.js b/test/js/node/test/sequential/test-net-connect-local-error.js new file mode 100644 index 000000000000..c18903001dd6 --- /dev/null +++ b/test/js/node/test/sequential/test-net-connect-local-error.js @@ -0,0 +1,46 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +// EADDRINUSE is expected to occur on FreeBSD +// Ref: https://github.com/nodejs/node/issues/13055 +const expectedErrorCodes = ['ECONNREFUSED', 'EADDRINUSE']; + +const optionsIPv4 = { + port: common.PORT, + family: 4, + localPort: common.PORT + 1, + localAddress: common.localhostIPv4, +}; + +const optionsIPv6 = { + host: '::1', + family: 6, + port: common.PORT + 2, + localPort: common.PORT + 3, + localAddress: '::1', +}; + +function onError(err, options) { + assert.ok(expectedErrorCodes.includes(err.code)); + assert.strictEqual(err.syscall, 'connect'); + assert.strictEqual(err.localPort, options.localPort); + assert.strictEqual(err.localAddress, options.localAddress); + assert.strictEqual( + err.message, + `connect ${err.code} ${err.address}:${err.port} ` + + `- Local (${err.localAddress}:${err.localPort})` + ); +} + +const clientIPv4 = net.connect(optionsIPv4); +clientIPv4.on('error', common.mustCall((err) => onError(err, optionsIPv4))); + +if (!common.hasIPv6) { + common.printSkipMessage('ipv6 part of test, no IPv6 support'); + return; +} + +const clientIPv6 = net.connect(optionsIPv6); +clientIPv6.on('error', common.mustCall((err) => onError(err, optionsIPv6)));