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();