Skip to content

Commit 58a636b

Browse files
ShogunPandarichardlau
authored andcommitted
net: add connection attempt events
PR-URL: #51045 Fixes: #48763 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Marco Ippolito <[email protected]>
1 parent cb3270e commit 58a636b

10 files changed

+549
-514
lines changed

doc/api/net.md

+42-2
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,47 @@ added: v0.1.90
684684
Emitted when a socket connection is successfully established.
685685
See [`net.createConnection()`][].
686686

687+
### Event: `'connectionAttempt'`
688+
689+
<!-- YAML
690+
added: REPLACEME
691+
-->
692+
693+
* `ip` {number} The IP which the socket is attempting to connect to.
694+
* `port` {number} The port which the socket is attempting to connect to.
695+
* `family` {number} The family of the IP. It can be `6` for IPv6 or `4` for IPv4.
696+
697+
Emitted when a new connection attempt is started. This may be emitted multiple times
698+
if the family autoselection algorithm is enabled in [`socket.connect(options)`][].
699+
700+
### Event: `'connectionAttemptFailed'`
701+
702+
<!-- YAML
703+
added: REPLACEME
704+
-->
705+
706+
* `ip` {number} The IP which the socket attempted to connect to.
707+
* `port` {number} The port which the socket attempted to connect to.
708+
* `family` {number} The family of the IP. It can be `6` for IPv6 or `4` for IPv4.
709+
\* `error` {Error} The error associated with the failure.
710+
711+
Emitted when a connection attempt failed. This may be emitted multiple times
712+
if the family autoselection algorithm is enabled in [`socket.connect(options)`][].
713+
714+
### Event: `'connectionAttemptTimeout'`
715+
716+
<!-- YAML
717+
added: REPLACEME
718+
-->
719+
720+
* `ip` {number} The IP which the socket attempted to connect to.
721+
* `port` {number} The port which the socket attempted to connect to.
722+
* `family` {number} The family of the IP. It can be `6` for IPv6 or `4` for IPv4.
723+
724+
Emitted when a connection attempt timed out. This is only emitted (and may be
725+
emitted multiple times) if the family autoselection algorithm is enabled
726+
in [`socket.connect(options)`][].
727+
687728
### Event: `'data'`
688729

689730
<!-- YAML
@@ -952,8 +993,7 @@ For TCP connections, available `options` are:
952993
obtained IPv6 and IPv4 addresses, in sequence, until a connection is established.
953994
The first returned AAAA address is tried first, then the first returned A address,
954995
then the second returned AAAA address and so on.
955-
Each connection attempt is given the amount of time specified by the `autoSelectFamilyAttemptTimeout`
956-
option before timing out and trying the next address.
996+
Each connection attempt (but the last one) is given the amount of time specified by the `autoSelectFamilyAttemptTimeout` option before timing out and trying the next address.
957997
Ignored if the `family` option is not `0` or if `localAddress` is set.
958998
Connection errors are not emitted if at least one connection succeeds.
959999
If all connections attempts fails, a single `AggregateError` with all failed attempts is emitted.

lib/net.js

+26-5
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,7 @@ function internalConnect(
10581058
}
10591059

10601060
debug('connect: attempting to connect to %s:%d (addressType: %d)', address, port, addressType);
1061+
self.emit('connectionAttempt', address, port, addressType);
10611062

10621063
if (addressType === 6 || addressType === 4) {
10631064
const req = new TCPConnectWrap();
@@ -1066,6 +1067,7 @@ function internalConnect(
10661067
req.port = port;
10671068
req.localAddress = localAddress;
10681069
req.localPort = localPort;
1070+
req.addressType = addressType;
10691071

10701072
if (addressType === 4)
10711073
err = self._handle.connect(req, address, port);
@@ -1149,13 +1151,15 @@ function internalConnectMultiple(context, canceled) {
11491151
}
11501152

11511153
debug('connect/multiple: attempting to connect to %s:%d (addressType: %d)', address, port, addressType);
1154+
self.emit('connectionAttempt', address, port, addressType);
11521155

11531156
const req = new TCPConnectWrap();
11541157
req.oncomplete = FunctionPrototypeBind(afterConnectMultiple, undefined, context, current);
11551158
req.address = address;
11561159
req.port = port;
11571160
req.localAddress = localAddress;
11581161
req.localPort = localPort;
1162+
req.addressType = addressType;
11591163

11601164
ArrayPrototypePush(self.autoSelectFamilyAttemptedAddresses, `${address}:${port}`);
11611165

@@ -1173,7 +1177,10 @@ function internalConnectMultiple(context, canceled) {
11731177
details = sockname.address + ':' + sockname.port;
11741178
}
11751179

1176-
ArrayPrototypePush(context.errors, new ExceptionWithHostPort(err, 'connect', address, port, details));
1180+
const ex = new ExceptionWithHostPort(err, 'connect', address, port, details);
1181+
ArrayPrototypePush(context.errors, ex);
1182+
1183+
self.emit('connectionAttemptFailed', address, port, addressType, ex);
11771184
internalConnectMultiple(context);
11781185
return;
11791186
}
@@ -1601,6 +1608,8 @@ function afterConnect(status, handle, req, readable, writable) {
16011608
ex.localAddress = req.localAddress;
16021609
ex.localPort = req.localPort;
16031610
}
1611+
1612+
self.emit('connectionAttemptFailed', req.address, req.port, req.addressType, ex);
16041613
self.destroy(ex);
16051614
}
16061615
}
@@ -1661,10 +1670,16 @@ function afterConnectMultiple(context, current, status, handle, req, readable, w
16611670

16621671
// Some error occurred, add to the list of exceptions
16631672
if (status !== 0) {
1664-
ArrayPrototypePush(context.errors, createConnectionError(req, status));
1673+
const ex = createConnectionError(req, status);
1674+
ArrayPrototypePush(context.errors, ex);
1675+
1676+
self.emit('connectionAttemptFailed', req.address, req.port, req.addressType, ex);
1677+
1678+
// Try the next address, unless we were aborted
1679+
if (context.socket.connecting) {
1680+
internalConnectMultiple(context, status === UV_ECANCELED);
1681+
}
16651682

1666-
// Try the next address
1667-
internalConnectMultiple(context, status === UV_ECANCELED);
16681683
return;
16691684
}
16701685

@@ -1681,10 +1696,16 @@ function afterConnectMultiple(context, current, status, handle, req, readable, w
16811696

16821697
function internalConnectMultipleTimeout(context, req, handle) {
16831698
debug('connect/multiple: connection to %s:%s timed out', req.address, req.port);
1699+
context.socket.emit('connectionAttemptTimeout', req.address, req.port, req.addressType);
1700+
16841701
req.oncomplete = undefined;
16851702
ArrayPrototypePush(context.errors, createConnectionError(req, UV_ETIMEDOUT));
16861703
handle.close();
1687-
internalConnectMultiple(context);
1704+
1705+
// Try the next address, unless we were aborted
1706+
if (context.socket.connecting) {
1707+
internalConnectMultiple(context);
1708+
}
16881709
}
16891710

16901711
function addServerAbortSignalOption(self, options) {

test/common/dns.js

+21
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const assert = require('assert');
44
const os = require('os');
5+
const { isIP } = require('net');
56

67
const types = {
78
A: 1,
@@ -309,6 +310,25 @@ function errorLookupMock(code = mockedErrorCode, syscall = mockedSysCall) {
309310
};
310311
}
311312

313+
function createMockedLookup(...addresses) {
314+
addresses = addresses.map((address) => ({ address: address, family: isIP(address) }));
315+
316+
// Create a DNS server which replies with a AAAA and a A record for the same host
317+
return function lookup(hostname, options, cb) {
318+
if (options.all === true) {
319+
process.nextTick(() => {
320+
cb(null, addresses);
321+
});
322+
323+
return;
324+
}
325+
326+
process.nextTick(() => {
327+
cb(null, addresses[0].address, addresses[0].family);
328+
});
329+
};
330+
}
331+
312332
module.exports = {
313333
types,
314334
classes,
@@ -317,4 +337,5 @@ module.exports = {
317337
errorLookupMock,
318338
mockedErrorCode,
319339
mockedSysCall,
340+
createMockedLookup,
320341
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { addresses: { INET6_IP, INET4_IP } } = require('../common/internet');
5+
const { createMockedLookup } = require('../common/dns');
6+
7+
const assert = require('assert');
8+
const { createConnection } = require('net');
9+
10+
//
11+
// When testing this is MacOS, remember that the last connection will have no timeout at Node.js
12+
// level but only at operating system one.
13+
//
14+
// The default for MacOS is 75 seconds. It can be changed by doing:
15+
//
16+
// sudo sysctl net.inet.tcp.keepinit=VALUE_IN_MS
17+
//
18+
19+
// Test that all failure events are emitted when trying a single IP (which means autoselectfamily is bypassed)
20+
{
21+
const pass = common.mustCallAtLeast(1);
22+
23+
const connection = createConnection({
24+
host: 'example.org',
25+
port: 10,
26+
lookup: createMockedLookup(INET4_IP),
27+
autoSelectFamily: true,
28+
autoSelectFamilyAttemptTimeout: 10,
29+
});
30+
31+
connection.on('connectionAttempt', (address, port, family) => {
32+
assert.strictEqual(address, INET4_IP);
33+
assert.strictEqual(port, 10);
34+
assert.strictEqual(family, 4);
35+
36+
pass();
37+
});
38+
39+
connection.on('connectionAttemptFailed', (address, port, family, error) => {
40+
assert.strictEqual(address, INET4_IP);
41+
assert.strictEqual(port, 10);
42+
assert.strictEqual(family, 4);
43+
44+
assert.ok(
45+
error.code.match(/ECONNREFUSED|ENETUNREACH|EHOSTUNREACH|ETIMEDOUT/),
46+
`Received unexpected error code ${error.code}`,
47+
);
48+
49+
pass();
50+
});
51+
52+
connection.on('ready', () => {
53+
pass();
54+
connection.destroy();
55+
});
56+
57+
connection.on('error', () => {
58+
pass();
59+
connection.destroy();
60+
});
61+
62+
setTimeout(() => {
63+
pass();
64+
process.exit(0);
65+
}, 5000).unref();
66+
67+
}
68+
69+
// Test that all events are emitted when trying multiple IPs
70+
{
71+
const pass = common.mustCallAtLeast(1);
72+
73+
const connection = createConnection({
74+
host: 'example.org',
75+
port: 10,
76+
lookup: createMockedLookup(INET6_IP, INET4_IP),
77+
autoSelectFamily: true,
78+
autoSelectFamilyAttemptTimeout: 10,
79+
});
80+
81+
const addresses = [
82+
{ address: INET6_IP, port: 10, family: 6 },
83+
{ address: INET6_IP, port: 10, family: 6 },
84+
{ address: INET4_IP, port: 10, family: 4 },
85+
{ address: INET4_IP, port: 10, family: 4 },
86+
];
87+
88+
connection.on('connectionAttempt', (address, port, family) => {
89+
const expected = addresses.shift();
90+
91+
assert.strictEqual(address, expected.address);
92+
assert.strictEqual(port, expected.port);
93+
assert.strictEqual(family, expected.family);
94+
95+
pass();
96+
});
97+
98+
connection.on('connectionAttemptFailed', (address, port, family, error) => {
99+
const expected = addresses.shift();
100+
101+
assert.strictEqual(address, expected.address);
102+
assert.strictEqual(port, expected.port);
103+
assert.strictEqual(family, expected.family);
104+
105+
assert.ok(
106+
error.code.match(/ECONNREFUSED|ENETUNREACH|EHOSTUNREACH|ETIMEDOUT/),
107+
`Received unexpected error code ${error.code}`,
108+
);
109+
110+
pass();
111+
});
112+
113+
connection.on('ready', () => {
114+
pass();
115+
connection.destroy();
116+
});
117+
118+
connection.on('error', () => {
119+
pass();
120+
connection.destroy();
121+
});
122+
123+
setTimeout(() => {
124+
pass();
125+
process.exit(0);
126+
}, 5000).unref();
127+
128+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { addresses: { INET6_IP, INET4_IP } } = require('../common/internet');
5+
const { createMockedLookup } = require('../common/dns');
6+
7+
const assert = require('assert');
8+
const { createConnection } = require('net');
9+
10+
//
11+
// When testing this is MacOS, remember that the last connection will have no timeout at Node.js
12+
// level but only at operating system one.
13+
//
14+
// The default for MacOS is 75 seconds. It can be changed by doing:
15+
//
16+
// sudo sysctl net.inet.tcp.keepinit=VALUE_IN_MS
17+
//
18+
// Depending on the network, it might be impossible to obtain a timeout in 10ms,
19+
// which is the minimum value allowed by network family autoselection.
20+
// At the time of writing (Dec 2023), the network times out on local machine and in the Node CI,
21+
// but it does not on GitHub actions runner.
22+
// Therefore, after five seconds we just consider this test as passed.
23+
24+
// Test that if a connection attempt times out and the socket is destroyed before the
25+
// next attempt starts then the process does not crash
26+
{
27+
const connection = createConnection({
28+
host: 'example.org',
29+
port: 443,
30+
lookup: createMockedLookup(INET4_IP, INET6_IP),
31+
autoSelectFamily: true,
32+
autoSelectFamilyAttemptTimeout: 10,
33+
});
34+
35+
const pass = common.mustCall();
36+
37+
connection.on('connectionAttemptTimeout', (address, port, family) => {
38+
assert.strictEqual(address, INET4_IP);
39+
assert.strictEqual(port, 443);
40+
assert.strictEqual(family, 4);
41+
connection.destroy();
42+
pass();
43+
});
44+
45+
connection.on('ready', () => {
46+
pass();
47+
connection.destroy();
48+
});
49+
50+
setTimeout(() => {
51+
pass();
52+
process.exit(0);
53+
}, 5000).unref();
54+
}

test/internet/test-net-autoselectfamily-timeout-close.js

+9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ const { addresses } = require('../common/internet');
66
const assert = require('assert');
77
const { connect } = require('net');
88

9+
//
10+
// When testing this is MacOS, remember that the last connection will have no timeout at Node.js
11+
// level but only at operating system one.
12+
//
13+
// The default for MacOS is 75 seconds. It can be changed by doing:
14+
//
15+
// sudo sysctl net.inet.tcp.keepinit=VALUE_IN_MS
16+
//
17+
918
// Test that when all errors are returned when no connections succeeded and that the close event is emitted
1019
{
1120
const connection = connect({

0 commit comments

Comments
 (0)