Skip to content

Commit 58a636b

Browse files
ShogunPandarichardlau
authored andcommitted
net: add connection attempt events
PR-URL: #51045Fixes: #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‎

Lines changed: 42 additions & 2 deletions
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‎

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
constreq=newTCPConnectWrap();
@@ -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
constreq=newTCPConnectWrap();
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,newExceptionWithHostPort(err,'connect',address,port,details));
1180+
constex=newExceptionWithHostPort(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+
constex=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
functioninternalConnectMultipleTimeout(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
functionaddServerAbortSignalOption(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
constassert=require('assert');
44
constos=require('os');
5+
const{ isIP }=require('net');
56

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

313+
functioncreateMockedLookup(...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+
returnfunctionlookup(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: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use strict';
2+
3+
constcommon=require('../common');
4+
const{addresses: {INET6_IP,INET4_IP}}=require('../common/internet');
5+
const{ createMockedLookup }=require('../common/dns');
6+
7+
constassert=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+
constpass=common.mustCallAtLeast(1);
22+
23+
constconnection=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+
constpass=common.mustCallAtLeast(1);
72+
73+
constconnection=createConnection({
74+
host: 'example.org',
75+
port: 10,
76+
lookup: createMockedLookup(INET6_IP,INET4_IP),
77+
autoSelectFamily: true,
78+
autoSelectFamilyAttemptTimeout: 10,
79+
});
80+
81+
constaddresses=[
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+
constexpected=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+
constexpected=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+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use strict';
2+
3+
constcommon=require('../common');
4+
const{addresses: {INET6_IP,INET4_IP}}=require('../common/internet');
5+
const{ createMockedLookup }=require('../common/dns');
6+
7+
constassert=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+
constconnection=createConnection({
28+
host: 'example.org',
29+
port: 443,
30+
lookup: createMockedLookup(INET4_IP,INET6_IP),
31+
autoSelectFamily: true,
32+
autoSelectFamilyAttemptTimeout: 10,
33+
});
34+
35+
constpass=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‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ const{addresses } = require('../common/internet');
66
constassert=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
constconnection=connect({

0 commit comments

Comments
(0)