Skip to content

Commit 669b81c

Browse files
LinkgoronMylesBorins
authored andcommitted
net,tls: add abort signal support to connect
Add documentation for net.connect AbortSignal, and add the support to tls.connect as well PR-URL: #37735 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 8792c7c commit 669b81c

7 files changed

+388
-0
lines changed

doc/api/net.md

+6
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

+2
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

+45
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,48 @@ const { getEventListeners } = require('events');
240240
req.on('close', common.mustCall(() => server.close()));
241241
}));
242242
}
243+
244+
245+
// Destroy ClientHttpSession with AbortSignal
246+
{
247+
function testH2ConnectAbort(secure) {
248+
const server = secure ? h2.createSecureServer() : h2.createServer();
249+
const controller = new AbortController();
250+
251+
server.on('stream', common.mustNotCall());
252+
server.listen(0, common.mustCall(() => {
253+
const { signal } = controller;
254+
const protocol = secure ? 'https' : 'http';
255+
const client = h2.connect(`${protocol}://localhost:${server.address().port}`, {
256+
signal,
257+
});
258+
client.on('close', common.mustCall());
259+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
260+
261+
client.on('error', common.mustCall(common.mustCall((err) => {
262+
assert.strictEqual(err.code, 'ABORT_ERR');
263+
assert.strictEqual(err.name, 'AbortError');
264+
})));
265+
266+
const req = client.request({}, {});
267+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
268+
269+
req.on('error', common.mustCall((err) => {
270+
assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_CANCEL');
271+
assert.strictEqual(err.name, 'Error');
272+
assert.strictEqual(req.aborted, false);
273+
assert.strictEqual(req.destroyed, true);
274+
}));
275+
req.on('close', common.mustCall(() => server.close()));
276+
277+
assert.strictEqual(req.aborted, false);
278+
assert.strictEqual(req.destroyed, false);
279+
// Signal listener attached
280+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
281+
282+
controller.abort();
283+
}));
284+
}
285+
testH2ConnectAbort(false);
286+
testH2ConnectAbort(true);
287+
}

test/parallel/test-https-abortcontroller.js

+55
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const options = {
1414
cert: fixtures.readKey('agent1-cert.pem')
1515
};
1616

17+
// Check async post-aborted
1718
(async () => {
1819
const { port, server } = await new Promise((resolve) => {
1920
const server = https.createServer(options, () => {});
@@ -38,3 +39,57 @@ const options = {
3839
server.close();
3940
}
4041
})().then(common.mustCall());
42+
43+
// Check sync post-aborted signal
44+
(async () => {
45+
const { port, server } = await new Promise((resolve) => {
46+
const server = https.createServer(options, () => {});
47+
server.listen(0, () => resolve({ port: server.address().port, server }));
48+
});
49+
try {
50+
const ac = new AbortController();
51+
const { signal } = ac;
52+
const req = https.request({
53+
host: 'localhost',
54+
port,
55+
path: '/',
56+
method: 'GET',
57+
rejectUnauthorized: false,
58+
signal,
59+
});
60+
assert.strictEqual(getEventListeners(ac.signal, 'abort').length, 1);
61+
ac.abort();
62+
const [ err ] = await once(req, 'error');
63+
assert.strictEqual(err.name, 'AbortError');
64+
assert.strictEqual(err.code, 'ABORT_ERR');
65+
} finally {
66+
server.close();
67+
}
68+
})().then(common.mustCall());
69+
70+
// Check pre-aborted signal
71+
(async () => {
72+
const { port, server } = await new Promise((resolve) => {
73+
const server = https.createServer(options, () => {});
74+
server.listen(0, () => resolve({ port: server.address().port, server }));
75+
});
76+
try {
77+
const ac = new AbortController();
78+
const { signal } = ac;
79+
ac.abort();
80+
const req = https.request({
81+
host: 'localhost',
82+
port,
83+
path: '/',
84+
method: 'GET',
85+
rejectUnauthorized: false,
86+
signal,
87+
});
88+
assert.strictEqual(getEventListeners(ac.signal, 'abort').length, 0);
89+
const [ err ] = await once(req, 'error');
90+
assert.strictEqual(err.name, 'AbortError');
91+
assert.strictEqual(err.code, 'ABORT_ERR');
92+
} finally {
93+
server.close();
94+
}
95+
})().then(common.mustCall());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
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+
async function agentAsParamPreAbort() {
68+
const ac = new AbortController();
69+
const { signal } = ac;
70+
ac.abort();
71+
const request = https.get({
72+
port: server.address().port,
73+
path: '/hello',
74+
agent: agent,
75+
signal,
76+
});
77+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
78+
const [err] = await once(request, 'error');
79+
assert.strictEqual(err.name, 'AbortError');
80+
}
81+
82+
await postCreateConnection();
83+
await preCreateConnection();
84+
await agentAsParam();
85+
await agentAsParamPreAbort();
86+
server.close();
87+
}));
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+
}));

0 commit comments

Comments
 (0)