From 413dffc691eae3b63983415671cc236eb1705622 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Fri, 6 Feb 2026 11:40:11 +0100 Subject: [PATCH 1/2] tools: enforce removal of `lts-watch-*` labels on release proposals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/61672 Reviewed-By: Michaƫl Zasso Reviewed-By: Richard Lau Reviewed-By: Colin Ihrig Reviewed-By: Marco Ippolito Reviewed-By: Rafael Gonzaga Reviewed-By: Tierney Cyren --- .github/workflows/lint-release-proposal.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint-release-proposal.yml b/.github/workflows/lint-release-proposal.yml index 3d87cb6e7b3f58..0c95ef406903b8 100644 --- a/.github/workflows/lint-release-proposal.yml +++ b/.github/workflows/lint-release-proposal.yml @@ -92,9 +92,15 @@ jobs: "/repos/${GITHUB_REPOSITORY}/compare/v${MAJOR}.x...$GITHUB_SHA" --paginate \ | node tools/actions/lint-release-proposal-commit-list.mjs "$CHANGELOG_PATH" "$GITHUB_SHA" \ | while IFS= read -r PR_URL; do - LABEL="dont-land-on-v${MAJOR}.x" gh pr view \ + DONT_LAND_LABEL="dont-land-on-v${MAJOR}.x" LTS_WATCH_LABEL="lts-watch-v${MAJOR}.x" gh pr view \ --json labels,url \ - --jq 'if (.labels|map(.name==env.LABEL)|any) then error("\(.url) has the \(env.LABEL) label, forbidding it to be in this release proposal") end' \ + --jq ' + if (.labels|any(.name==env.DONT_LAND_LABEL)) then + error("\(.url) has the \(env.DONT_LAND_LABEL) label, forbidding it to be in this release proposal") + elif (.labels|any(.name==env.LTS_WATCH_LABEL)) then + error("\(.url) has the \(env.LTS_WATCH_LABEL) label, please remove the label now that the PR is included in a release proposal") + end + ' \ "$PR_URL" > /dev/null done shell: bash # See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference, we want the pipefail option. From d8c00ad316e2ae25322b2f1c405a902384d2867d Mon Sep 17 00:00:00 2001 From: RajeshKumar11 Date: Fri, 6 Feb 2026 20:00:07 +0530 Subject: [PATCH 2/2] net: defer synchronous destroy calls in internalConnect Defer socket.destroy() calls in internalConnect and internalConnectMultiple to the next tick. This ensures that error handlers have a chance to be set up before errors are emitted, particularly important when using http.request with a custom lookup function that returns synchronously. Previously, if a synchronous lookup function returned an IP that triggered an immediate error (e.g., via blockList), the error would be emitted before the HTTP client had set up its error handler (which happens via process.nextTick in onSocket). This caused unhandled 'error' events. Fixes: https://github.com/nodejs/node/issues/48771 PR-URL: https://github.com/nodejs/node/pull/61658 Refs: https://github.com/nodejs/node/pull/51038 Reviewed-By: Tim Perry Reviewed-By: Jason Zhang --- lib/net.js | 18 +++++-- ...est-http-request-lookup-error-catchable.js | 47 +++++++++++++++++++ 2 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 test/parallel/test-http-request-lookup-error-catchable.js diff --git a/lib/net.js b/lib/net.js index e22ef4bfc4bff0..cbde238ba0a2ce 100644 --- a/lib/net.js +++ b/lib/net.js @@ -1125,7 +1125,7 @@ function internalConnect( err = checkBindError(err, localPort, self._handle); if (err) { const ex = new ExceptionWithHostPort(err, 'bind', localAddress, localPort); - self.destroy(ex); + process.nextTick(emitErrorAndDestroy, self, ex); return; } } @@ -1135,7 +1135,7 @@ function internalConnect( if (addressType === 6 || addressType === 4) { if (self.blockList?.check(address, `ipv${addressType}`)) { - self.destroy(new ERR_IP_BLOCKED(address)); + process.nextTick(emitErrorAndDestroy, self, new ERR_IP_BLOCKED(address)); return; } const req = new TCPConnectWrap(); @@ -1167,12 +1167,20 @@ function internalConnect( } const ex = new ExceptionWithHostPort(err, 'connect', address, port, details); - self.destroy(ex); + process.nextTick(emitErrorAndDestroy, self, ex); } else if ((addressType === 6 || addressType === 4) && hasObserver('net')) { startPerf(self, kPerfHooksNetConnectContext, { type: 'net', name: 'connect', detail: { host: address, port } }); } } +// Helper function to defer socket destruction to the next tick. +// This ensures that error handlers have a chance to be set up +// before the error is emitted, particularly important when using +// http.request with a custom lookup function. +function emitErrorAndDestroy(self, err) { + self.destroy(err); +} + function internalConnectMultiple(context, canceled) { clearTimeout(context[kTimeout]); @@ -1186,11 +1194,11 @@ function internalConnectMultiple(context, canceled) { // All connections have been tried without success, destroy with error if (canceled || context.current === context.addresses.length) { if (context.errors.length === 0) { - self.destroy(new ERR_SOCKET_CONNECTION_TIMEOUT()); + process.nextTick(emitErrorAndDestroy, self, new ERR_SOCKET_CONNECTION_TIMEOUT()); return; } - self.destroy(new NodeAggregateError(context.errors)); + process.nextTick(emitErrorAndDestroy, self, new NodeAggregateError(context.errors)); return; } diff --git a/test/parallel/test-http-request-lookup-error-catchable.js b/test/parallel/test-http-request-lookup-error-catchable.js new file mode 100644 index 00000000000000..905f841c77c096 --- /dev/null +++ b/test/parallel/test-http-request-lookup-error-catchable.js @@ -0,0 +1,47 @@ +'use strict'; +const common = require('../common'); +const http = require('http'); +const net = require('net'); + +// This test verifies that errors occurring synchronously during connection +// when using http.request with a custom lookup function and blockList +// can be caught by the error handler. +// Regression test for https://github.com/nodejs/node/issues/48771 + +// The issue occurs when: +// 1. http.request() is called with a custom synchronous lookup function +// 2. The lookup returns an IP that triggers a synchronous error (e.g., blockList) +// 3. The error is emitted before http's error handler is set up (via nextTick) +// +// The fix defers socket.destroy() calls in internalConnect to the next tick, +// giving http.request() time to set up its error handlers. + +const blockList = new net.BlockList(); +blockList.addAddress(common.localhostIPv4); + +// Synchronous lookup that returns the blocked IP +const lookup = (_hostname, _options, callback) => { + callback(null, common.localhostIPv4, 4); +}; + +const req = http.request({ + host: 'example.com', + port: 80, + lookup, + family: 4, // Force IPv4 to use simple lookup path + createConnection: (opts) => { + // Pass blockList to trigger synchronous ERR_IP_BLOCKED error + return net.createConnection({ ...opts, blockList }); + }, +}, common.mustNotCall()); + +// This error handler must be called. +// Without the fix, the error would be emitted before http.request() +// returns, causing an unhandled 'error' event. +req.on('error', common.mustCall((err) => { + if (err.code !== 'ERR_IP_BLOCKED') { + throw new Error(`Expected ERR_IP_BLOCKED but got ${err.code}`); + } +})); + +req.end();