Skip to content

Commit 1fd2961

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 c2a792f commit 1fd2961

File tree

6 files changed

+337
-0
lines changed

6 files changed

+337
-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: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto)
4+
common.skip('missing crypto');
5+
6+
const assert = require('assert');
7+
const https = require('https');
8+
const { once } = require('events');
9+
const Agent = https.Agent;
10+
const fixtures = require('../common/fixtures');
11+
12+
const { getEventListeners } = require('events');
13+
const agent = new Agent();
14+
15+
const options = {
16+
key: fixtures.readKey('agent1-key.pem'),
17+
cert: fixtures.readKey('agent1-cert.pem')
18+
};
19+
20+
const server = https.createServer(options);
21+
22+
server.listen(0, common.mustCall(async () => {
23+
const port = server.address().port;
24+
const host = 'localhost';
25+
const options = {
26+
port: port,
27+
host: host,
28+
rejectUnauthorized: false,
29+
_agentKey: agent.getName({ port, host })
30+
};
31+
32+
async function postCreateConnection() {
33+
const ac = new AbortController();
34+
const { signal } = ac;
35+
const connection = agent.createConnection({ ...options, signal });
36+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
37+
ac.abort();
38+
const [err] = await once(connection, 'error');
39+
assert.strictEqual(err.name, 'AbortError');
40+
}
41+
42+
async function preCreateConnection() {
43+
const ac = new AbortController();
44+
const { signal } = ac;
45+
ac.abort();
46+
const connection = agent.createConnection({ ...options, signal });
47+
const [err] = await once(connection, 'error');
48+
assert.strictEqual(err.name, 'AbortError');
49+
}
50+
51+
// Blocked on https://github.com/nodejs/node/pull/37730
52+
// async function agentAsParam() {
53+
// const ac = new AbortController();
54+
// const { signal } = ac;
55+
// const request = https.get({
56+
// port: server.address().port,
57+
// path: '/hello',
58+
// agent: agent,
59+
// signal,
60+
// });
61+
// assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
62+
// ac.abort();
63+
// const [err] = await once(request, 'error');
64+
// assert.strictEqual(err.name, 'AbortError');
65+
// }
66+
67+
// // Blocked on https://github.com/nodejs/node/pull/37730
68+
// async function agentAsParamPreAbort() {
69+
// const ac = new AbortController();
70+
// const { signal } = ac;
71+
// ac.abort();
72+
// const request = https.get({
73+
// port: server.address().port,
74+
// path: '/hello',
75+
// agent: agent,
76+
// signal,
77+
// });
78+
// assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
79+
// const [err] = await once(request, 'error');
80+
// assert.strictEqual(err.name, 'AbortError');
81+
// }
82+
83+
await postCreateConnection();
84+
await preCreateConnection();
85+
// Blocked on https://github.com/nodejs/node/pull/37730
86+
// await agentAsParam();
87+
// Blocked on https://github.com/nodejs/node/pull/37730
88+
// await agentAsParamPreAbort();
89+
server.close();
90+
}));
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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, once } = require('events');
7+
8+
const liveConnections = 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', (connection) => {
15+
liveConnections.add(connection);
16+
connection.on('close', () => {
17+
liveConnections.delete(connection);
18+
});
19+
});
20+
21+
const assertAbort = async (socket, testName) => {
22+
try {
23+
await once(socket, 'close');
24+
assert.fail(`close ${testName} should have thrown`);
25+
} catch (err) {
26+
assert.strictEqual(err.name, 'AbortError');
27+
}
28+
};
29+
30+
async function postAbort() {
31+
const ac = new AbortController();
32+
const { signal } = ac;
33+
const socket = net.connect(socketOptions(signal));
34+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
35+
ac.abort();
36+
await assertAbort(socket, 'postAbort');
37+
}
38+
39+
async function preAbort() {
40+
const ac = new AbortController();
41+
const { signal } = ac;
42+
ac.abort();
43+
const socket = net.connect(socketOptions(signal));
44+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
45+
await assertAbort(socket, 'preAbort');
46+
}
47+
48+
async function tickAbort() {
49+
const ac = new AbortController();
50+
const { signal } = ac;
51+
setImmediate(() => ac.abort());
52+
const socket = net.connect(socketOptions(signal));
53+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
54+
await assertAbort(socket, 'tickAbort');
55+
}
56+
57+
async function testConstructor() {
58+
const ac = new AbortController();
59+
const { signal } = ac;
60+
ac.abort();
61+
const socket = new net.Socket(socketOptions(signal));
62+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
63+
await assertAbort(socket, 'testConstructor');
64+
}
65+
66+
async function testConstructorPost() {
67+
const ac = new AbortController();
68+
const { signal } = ac;
69+
const socket = new net.Socket(socketOptions(signal));
70+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
71+
ac.abort();
72+
await assertAbort(socket, 'testConstructorPost');
73+
}
74+
75+
async function testConstructorPostTick() {
76+
const ac = new AbortController();
77+
const { signal } = ac;
78+
const socket = new net.Socket(socketOptions(signal));
79+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
80+
setImmediate(() => ac.abort());
81+
await assertAbort(socket, 'testConstructorPostTick');
82+
}
83+
84+
await postAbort();
85+
await preAbort();
86+
await tickAbort();
87+
await testConstructor();
88+
await testConstructorPost();
89+
await testConstructorPostTick();
90+
91+
// Killing the net.socket without connecting hangs the server.
92+
for (const connection of liveConnections) {
93+
connection.destroy();
94+
}
95+
server.close(common.mustCall());
96+
}));
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto)
4+
common.skip('missing crypto');
5+
6+
const tls = require('tls');
7+
const assert = require('assert');
8+
const fixtures = require('../common/fixtures');
9+
const { getEventListeners, once } = require('events');
10+
11+
const serverOptions = {
12+
key: fixtures.readKey('agent1-key.pem'),
13+
cert: fixtures.readKey('agent1-cert.pem')
14+
};
15+
const server = tls.createServer(serverOptions);
16+
server.listen(0, common.mustCall(async () => {
17+
const port = server.address().port;
18+
const host = 'localhost';
19+
const connectOptions = (signal) => ({
20+
port,
21+
host,
22+
signal,
23+
rejectUnauthorized: false,
24+
});
25+
26+
const assertAbort = async (socket, testName) => {
27+
try {
28+
await once(socket, 'close');
29+
assert.fail(`close ${testName} should have thrown`);
30+
} catch (err) {
31+
assert.strictEqual(err.name, 'AbortError');
32+
}
33+
};
34+
35+
async function postAbort() {
36+
const ac = new AbortController();
37+
const { signal } = ac;
38+
const socket = tls.connect(connectOptions(signal));
39+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
40+
ac.abort();
41+
await assertAbort(socket, 'postAbort');
42+
}
43+
44+
async function preAbort() {
45+
const ac = new AbortController();
46+
const { signal } = ac;
47+
ac.abort();
48+
const socket = tls.connect(connectOptions(signal));
49+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
50+
await assertAbort(socket, 'preAbort');
51+
}
52+
53+
async function tickAbort() {
54+
const ac = new AbortController();
55+
const { signal } = ac;
56+
const socket = tls.connect(connectOptions(signal));
57+
setImmediate(() => ac.abort());
58+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
59+
await assertAbort(socket, 'tickAbort');
60+
}
61+
62+
async function testConstructor() {
63+
const ac = new AbortController();
64+
const { signal } = ac;
65+
ac.abort();
66+
const socket = new tls.TLSSocket(undefined, connectOptions(signal));
67+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
68+
await assertAbort(socket, 'testConstructor');
69+
}
70+
71+
async function testConstructorPost() {
72+
const ac = new AbortController();
73+
const { signal } = ac;
74+
const socket = new tls.TLSSocket(undefined, connectOptions(signal));
75+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
76+
ac.abort();
77+
await assertAbort(socket, 'testConstructorPost');
78+
}
79+
80+
async function testConstructorPostTick() {
81+
const ac = new AbortController();
82+
const { signal } = ac;
83+
const socket = new tls.TLSSocket(undefined, connectOptions(signal));
84+
setImmediate(() => ac.abort());
85+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
86+
await assertAbort(socket, 'testConstructorPostTick');
87+
}
88+
89+
await postAbort();
90+
await preAbort();
91+
await tickAbort();
92+
await testConstructor();
93+
await testConstructorPost();
94+
await testConstructorPostTick();
95+
96+
server.close(common.mustCall());
97+
}));

0 commit comments

Comments
 (0)