Skip to content

Commit de721be

Browse files
committed
net: added connection attempt events
1 parent 6e90fed commit de721be

9 files changed

+463
-514
lines changed

doc/api/net.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,47 @@ added: v0.1.90
691691
Emitted when a socket connection is successfully established.
692692
See [`net.createConnection()`][].
693693

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

696737
<!-- YAML
@@ -963,8 +1004,7 @@ For TCP connections, available `options` are:
9631004
obtained IPv6 and IPv4 addresses, in sequence, until a connection is established.
9641005
The first returned AAAA address is tried first, then the first returned A address,
9651006
then the second returned AAAA address and so on.
966-
Each connection attempt is given the amount of time specified by the `autoSelectFamilyAttemptTimeout`
967-
option before timing out and trying the next address.
1007+
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.
9681008
Ignored if the `family` option is not `0` or if `localAddress` is set.
9691009
Connection errors are not emitted if at least one connection succeeds.
9701010
If all connections attempts fails, a single `AggregateError` with all failed attempts is emitted.

lib/net.js

Lines changed: 26 additions & 5 deletions
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

Lines changed: 21 additions & 0 deletions
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
};
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
// Test that all events are emitted when trying a single IP (which means autoselectfamily is bypassed)
11+
{
12+
const connection = createConnection({
13+
host: 'example.org',
14+
port: 10,
15+
lookup: createMockedLookup(INET4_IP),
16+
autoSelectFamily: true,
17+
autoSelectFamilyAttemptTimeout: 10,
18+
});
19+
20+
connection.on('connectionAttempt', common.mustCall((address, port, family) => {
21+
assert.strictEqual(address, INET4_IP);
22+
assert.strictEqual(port, 10);
23+
assert.strictEqual(family, 4);
24+
}));
25+
26+
connection.on('connectionAttemptFailed', common.mustCall((address, port, family, error) => {
27+
assert.strictEqual(address, INET4_IP);
28+
assert.strictEqual(port, 10);
29+
assert.strictEqual(family, 4);
30+
31+
assert.ok(error.code.match(/ECONNREFUSED|EHOSTUNREACH|ETIMEDOUT/));
32+
}));
33+
34+
connection.on('ready', common.mustNotCall());
35+
connection.on('error', common.mustCall());
36+
}
37+
38+
// Test that all events are emitted when trying multiple IPs
39+
{
40+
const connection = createConnection({
41+
host: 'example.org',
42+
port: 10,
43+
lookup: createMockedLookup(INET6_IP, INET4_IP),
44+
autoSelectFamily: true,
45+
autoSelectFamilyAttemptTimeout: 10,
46+
});
47+
48+
const addresses = [
49+
{ address: INET6_IP, port: 10, family: 6 },
50+
{ address: INET6_IP, port: 10, family: 6 },
51+
{ address: INET4_IP, port: 10, family: 4 },
52+
{ address: INET4_IP, port: 10, family: 4 },
53+
];
54+
55+
connection.on('connectionAttempt', common.mustCall((address, port, family) => {
56+
const expected = addresses.shift();
57+
58+
assert.strictEqual(address, expected.address);
59+
assert.strictEqual(port, expected.port);
60+
assert.strictEqual(family, expected.family);
61+
}, 2));
62+
63+
connection.on('connectionAttemptFailed', common.mustCall((address, port, family, error) => {
64+
const expected = addresses.shift();
65+
66+
assert.strictEqual(address, expected.address);
67+
assert.strictEqual(port, expected.port);
68+
assert.strictEqual(family, expected.family);
69+
70+
assert.ok(error.code.match(/ECONNREFUSED|EHOSTUNREACH|ETIMEDOUT/), `Received unexpected error code ${error.code}`);
71+
}, 2));
72+
73+
connection.on('ready', common.mustNotCall());
74+
connection.on('error', common.mustCall());
75+
}
76+
77+
// Test that if a connection attempt times out and the socket is destroyed before the
78+
// next attempt starts then the process does not crash
79+
{
80+
const connection = createConnection({
81+
host: 'example.org',
82+
port: 443,
83+
lookup: createMockedLookup(INET4_IP, INET6_IP),
84+
autoSelectFamily: true,
85+
autoSelectFamilyAttemptTimeout: 10,
86+
});
87+
88+
connection.on('connectionAttemptTimeout', common.mustCall((address, port, family) => {
89+
assert.strictEqual(address, INET4_IP);
90+
assert.strictEqual(port, 443);
91+
assert.strictEqual(family, 4);
92+
connection.destroy();
93+
}));
94+
95+
connection.on('ready', common.mustNotCall());
96+
}

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

Lines changed: 9 additions & 0 deletions
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)