Skip to content

Commit e099f3f

Browse files
committed
net,tls: add abort signal support to connect
Add documentation for net.connect AbortSignal, and add the support to tls.connect as well
1 parent bfa6e37 commit e099f3f

File tree

6 files changed

+383
-0
lines changed

6 files changed

+383
-0
lines changed

doc/api/net.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,10 @@ it to interact with the client.
496496
### `new net.Socket([options])`
497497
<!-- YAML
498498
added: v0.3.4
499+
changes:
500+
- version: REPLACEME
501+
pr-url: https://github.com/nodejs/node/pull/37735
502+
description: AbortSignal support was added.
499503
-->
500504

501505
* `options` {Object} Available options are:
@@ -508,6 +512,8 @@ added: v0.3.4
508512
otherwise ignored. **Default:** `false`.
509513
* `writable` {boolean} Allow writes on the socket when an `fd` is passed,
510514
otherwise ignored. **Default:** `false`.
515+
* `signal` {AbortSignal} An Abort signal that may be used to destroy the
516+
socket.
511517
* Returns: {net.Socket}
512518

513519
Creates a new socket object.

lib/_tls_wrap.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ function TLSSocket(socket, opts) {
515515
manualStart: true,
516516
highWaterMark: tlsOptions.highWaterMark,
517517
onread: !socket ? tlsOptions.onread : null,
518+
signal: tlsOptions.signal,
518519
}]);
519520

520521
// Proxy for API compatibility
@@ -1627,6 +1628,7 @@ exports.connect = function connect(...args) {
16271628
pskCallback: options.pskCallback,
16281629
highWaterMark: options.highWaterMark,
16291630
onread: options.onread,
1631+
signal: options.signal,
16301632
});
16311633

16321634
tlssock[kConnectOptions] = options;

test/parallel/test-http2-client-destroy.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const h2 = require('http2');
1010
const { kSocket } = require('internal/http2/util');
1111
const { kEvents } = require('internal/event_target');
1212
const Countdown = require('../common/countdown');
13+
const { getEventListeners } = require('events');
1314

1415
{
1516
const server = h2.createServer();
@@ -241,3 +242,48 @@ const Countdown = require('../common/countdown');
241242
req.on('close', common.mustCall(() => server.close()));
242243
}));
243244
}
245+
246+
247+
// Destroy ClientHttpSession with AbortSignal
248+
{
249+
function testH2ConnectAbort(secure) {
250+
const server = secure ? h2.createSecureServer() : h2.createServer();
251+
const controller = new AbortController();
252+
253+
server.on('stream', common.mustNotCall());
254+
server.listen(0, common.mustCall(() => {
255+
const { signal } = controller;
256+
const protocol = secure ? 'https' : 'http';
257+
const client = h2.connect(`${protocol}://localhost:${server.address().port}`, {
258+
signal,
259+
});
260+
client.on('close', common.mustCall());
261+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
262+
263+
client.on('error', common.mustCall(common.mustCall((err) => {
264+
assert.strictEqual(err.code, 'ABORT_ERR');
265+
assert.strictEqual(err.name, 'AbortError');
266+
})));
267+
268+
const req = client.request({}, {});
269+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
270+
271+
req.on('error', common.mustCall((err) => {
272+
assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_CANCEL');
273+
assert.strictEqual(err.name, 'Error');
274+
assert.strictEqual(req.aborted, false);
275+
assert.strictEqual(req.destroyed, true);
276+
}));
277+
req.on('close', common.mustCall(() => server.close()));
278+
279+
assert.strictEqual(req.aborted, false);
280+
assert.strictEqual(req.destroyed, false);
281+
// Signal listener attached
282+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
283+
284+
controller.abort();
285+
}));
286+
}
287+
testH2ConnectAbort(false);
288+
testH2ConnectAbort(true);
289+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const https = require('https');
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
const Agent = https.Agent;
8+
const fixtures = require('../common/fixtures');
9+
10+
const { getEventListeners } = require('events');
11+
const agent = new Agent();
12+
13+
const options = {
14+
key: fixtures.readKey('agent1-key.pem'),
15+
cert: fixtures.readKey('agent1-cert.pem')
16+
};
17+
18+
const server = https.createServer(options);
19+
20+
server.listen(0, common.mustCall(async () => {
21+
const port = server.address().port;
22+
const host = 'localhost';
23+
const options = {
24+
port: port,
25+
host: host,
26+
rejectUnauthorized: false,
27+
_agentKey: agent.getName({ port, host })
28+
};
29+
30+
function postCreateConnection() {
31+
return new Promise((res) => {
32+
const ac = new AbortController();
33+
const { signal } = ac;
34+
const connection = agent.createConnection({ ...options, signal });
35+
connection.on('error', common.mustCall((err) => {
36+
assert(err);
37+
assert.strictEqual(err.name, 'AbortError');
38+
res();
39+
}));
40+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
41+
ac.abort();
42+
});
43+
}
44+
45+
function preCreateConnection() {
46+
return new Promise((res) => {
47+
const ac = new AbortController();
48+
const { signal } = ac;
49+
ac.abort();
50+
const connection = agent.createConnection({ ...options, signal });
51+
connection.on('error', common.mustCall((err) => {
52+
assert(err);
53+
assert.strictEqual(err.name, 'AbortError');
54+
res();
55+
}));
56+
});
57+
}
58+
59+
// Blocked on https://github.com/nodejs/node/pull/37730
60+
// function agentAsParam() {
61+
// return new Promise((res) => {
62+
// const ac = new AbortController();
63+
// const { signal } = ac;
64+
// const request = https.get({
65+
// port: server.address().port,
66+
// path: '/hello',
67+
// agent: agent,
68+
// signal,
69+
// });
70+
// request.on('error', common.mustCall((err) => {
71+
// assert(err);
72+
// assert.strictEqual(err.name, 'AbortError');
73+
// res();
74+
// }));
75+
// assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
76+
// ac.abort();
77+
// });
78+
// }
79+
80+
// Blocked on https://github.com/nodejs/node/pull/37730
81+
// function agentAsParamPreAbort() {
82+
// return new Promise((res) => {
83+
// const ac = new AbortController();
84+
// const { signal } = ac;
85+
// ac.abort();
86+
// const request = https.get({
87+
// port: server.address().port,
88+
// path: '/hello',
89+
// agent: agent,
90+
// signal,
91+
// });
92+
// request.on('error', common.mustCall((err) => {
93+
// assert(err);
94+
// assert.strictEqual(err.name, 'AbortError');
95+
// res();
96+
// }));
97+
// assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
98+
// });
99+
// }
100+
101+
await postCreateConnection();
102+
await preCreateConnection();
103+
// Blocked on https://github.com/nodejs/node/pull/37730
104+
// await agentAsParam();
105+
// Blocked on https://github.com/nodejs/node/pull/37730
106+
// await agentAsParamPreAbort();
107+
server.close();
108+
}));
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
'use strict';
2+
const common = require('../common');
3+
const net = require('net');
4+
const assert = require('assert');
5+
const server = net.createServer();
6+
const { getEventListeners } = require('events');
7+
8+
const connections = new Set();
9+
10+
server.listen(0, common.mustCall(async () => {
11+
const port = server.address().port;
12+
const host = 'localhost';
13+
const socketOptions = (signal) => ({ port, host, signal });
14+
server.on('connection', function (conn) {
15+
connections.add(conn);
16+
conn.on('close', function () {
17+
connections.delete(conn);
18+
});
19+
});
20+
21+
const attachHandlers = (socket, res) => {
22+
socket.on('error', common.mustCall((err) => {
23+
assert.strictEqual(err.name, 'AbortError');
24+
}));
25+
socket.on('close', () => {
26+
res();
27+
});
28+
};
29+
30+
function postAbort() {
31+
return new Promise((res) => {
32+
const ac = new AbortController();
33+
const { signal } = ac;
34+
const socket = net.connect(socketOptions(signal));
35+
attachHandlers(socket, res);
36+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
37+
ac.abort();
38+
});
39+
}
40+
41+
function preAbort() {
42+
return new Promise((res) => {
43+
const ac = new AbortController();
44+
const { signal } = ac;
45+
ac.abort();
46+
const socket = net.connect(socketOptions(signal));
47+
attachHandlers(socket, res);
48+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
49+
});
50+
}
51+
52+
function tickAbort() {
53+
return new Promise((res) => {
54+
const ac = new AbortController();
55+
const { signal } = ac;
56+
setImmediate(() => ac.abort());
57+
const socket = net.connect(socketOptions(signal));
58+
attachHandlers(socket, res);
59+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
60+
});
61+
}
62+
63+
function testConstructor() {
64+
return new Promise((res) => {
65+
const ac = new AbortController();
66+
const { signal } = ac;
67+
ac.abort();
68+
const socket = new net.Socket(socketOptions(signal));
69+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
70+
attachHandlers(socket, res);
71+
});
72+
}
73+
74+
function testConstructorPost() {
75+
return new Promise((res) => {
76+
const ac = new AbortController();
77+
const { signal } = ac;
78+
const socket = new net.Socket(socketOptions(signal));
79+
attachHandlers(socket, res);
80+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
81+
ac.abort();
82+
});
83+
}
84+
85+
function testConstructorPostTick() {
86+
return new Promise((res) => {
87+
const ac = new AbortController();
88+
const { signal } = ac;
89+
const socket = new net.Socket(socketOptions(signal));
90+
attachHandlers(socket, res);
91+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
92+
setImmediate(() => {
93+
ac.abort();
94+
});
95+
});
96+
}
97+
98+
await postAbort();
99+
await preAbort();
100+
await tickAbort();
101+
await testConstructor();
102+
await testConstructorPost();
103+
await testConstructorPostTick();
104+
105+
// Killing the net.socket without connecting hangs the server.
106+
for (const connection of connections) {
107+
connection.destroy();
108+
}
109+
server.close(common.mustCall());
110+
}));

0 commit comments

Comments
 (0)