Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions lib/net.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand All @@ -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();
Expand Down Expand Up @@ -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]);
Expand All @@ -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;
}

Expand Down
47 changes: 47 additions & 0 deletions test/parallel/test-http-request-lookup-error-catchable.js
Original file line number Diff line number Diff line change
@@ -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();
Loading