Skip to content

Commit 5e0e0d2

Browse files
committed
http: fix double AbortSignal registration
Fix an issue where AbortSignals are registered twice
1 parent b9fd4eb commit 5e0e0d2

5 files changed

+177
-16
lines changed

lib/_http_client.js

+12-5
Original file line numberDiff line numberDiff line change
@@ -288,14 +288,20 @@ function ClientRequest(input, options, cb) {
288288
options.headers);
289289
}
290290

291+
let optsWithoutSignal = options;
292+
if (optsWithoutSignal.signal) {
293+
optsWithoutSignal = ObjectAssign({}, options);
294+
delete optsWithoutSignal.signal;
295+
}
296+
291297
// initiate connection
292298
if (this.agent) {
293-
this.agent.addRequest(this, options);
299+
this.agent.addRequest(this, optsWithoutSignal);
294300
} else {
295301
// No agent, default to Connection:close.
296302
this._last = true;
297303
this.shouldKeepAlive = false;
298-
if (typeof options.createConnection === 'function') {
304+
if (typeof optsWithoutSignal.createConnection === 'function') {
299305
const oncreate = once((err, socket) => {
300306
if (err) {
301307
process.nextTick(() => emitError(this, err));
@@ -305,16 +311,17 @@ function ClientRequest(input, options, cb) {
305311
});
306312

307313
try {
308-
const newSocket = options.createConnection(options, oncreate);
314+
const newSocket = optsWithoutSignal.createConnection(optsWithoutSignal,
315+
oncreate);
309316
if (newSocket) {
310317
oncreate(null, newSocket);
311318
}
312319
} catch (err) {
313320
oncreate(err);
314321
}
315322
} else {
316-
debug('CLIENT use net.createConnection', options);
317-
this.onSocket(net.createConnection(options));
323+
debug('CLIENT use net.createConnection', optsWithoutSignal);
324+
this.onSocket(net.createConnection(optsWithoutSignal));
318325
}
319326
}
320327
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const http = require('http');
5+
const Agent = require('_http_agent').Agent;
6+
const { getEventListeners } = require('events');
7+
const agent = new Agent();
8+
const server = http.createServer();
9+
10+
server.listen(0, common.mustCall(() => {
11+
const port = server.address().port;
12+
const host = 'localhost';
13+
const options = {
14+
port: port,
15+
host: host,
16+
rejectUnauthorized: false,
17+
_agentKey: agent.getName({ port, host })
18+
};
19+
20+
function postCreateConnection() {
21+
return new Promise((res) => {
22+
const ac = new AbortController();
23+
const { signal } = ac;
24+
const connection = agent.createConnection({ ...options, signal });
25+
connection.on('error', common.mustCall((err) => {
26+
assert(err);
27+
assert.strictEqual(err.name, 'AbortError');
28+
res();
29+
}));
30+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
31+
ac.abort();
32+
});
33+
}
34+
35+
function preCreateConnection() {
36+
return new Promise((res) => {
37+
const ac = new AbortController();
38+
const { signal } = ac;
39+
ac.abort();
40+
const connection = agent.createConnection({ ...options, signal });
41+
connection.on('error', common.mustCall((err) => {
42+
assert(err);
43+
assert.strictEqual(err.name, 'AbortError');
44+
res();
45+
}));
46+
});
47+
}
48+
49+
function agentAsParam() {
50+
return new Promise((res) => {
51+
const ac = new AbortController();
52+
const { signal } = ac;
53+
const request = http.get({
54+
port: server.address().port,
55+
path: '/hello',
56+
agent: agent,
57+
signal,
58+
});
59+
request.on('error', common.mustCall((err) => {
60+
assert(err);
61+
assert.strictEqual(err.name, 'AbortError');
62+
res();
63+
}));
64+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
65+
ac.abort();
66+
});
67+
}
68+
69+
function agentAsParamPreAbort() {
70+
return new Promise((res) => {
71+
const ac = new AbortController();
72+
const { signal } = ac;
73+
ac.abort();
74+
const request = http.get({
75+
port: server.address().port,
76+
path: '/hello',
77+
agent: agent,
78+
signal,
79+
});
80+
request.on('error', common.mustCall((err) => {
81+
assert(err);
82+
assert.strictEqual(err.name, 'AbortError');
83+
res();
84+
}));
85+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
86+
});
87+
}
88+
89+
postCreateConnection()
90+
.then(common.mustCall(preCreateConnection))
91+
.then(common.mustCall(agentAsParam))
92+
.then(common.mustCall(agentAsParamPreAbort))
93+
.then(common.mustCall(() => server.close()));
94+
}));

test/parallel/test-http-client-abort-destroy.js

+23-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const common = require('../common');
33
const http = require('http');
44
const assert = require('assert');
5+
const { getEventListeners } = require('events');
56

67
{
78
// abort
@@ -75,19 +76,39 @@ const assert = require('assert');
7576

7677
const server = http.createServer(common.mustNotCall());
7778
const controller = new AbortController();
78-
79+
const { signal } = controller;
7980
server.listen(0, common.mustCall(() => {
80-
const options = { port: server.address().port, signal: controller.signal };
81+
const options = { port: server.address().port, signal };
8182
const req = http.get(options, common.mustNotCall());
8283
req.on('error', common.mustCall((err) => {
8384
assert.strictEqual(err.code, 'ABORT_ERR');
8485
assert.strictEqual(err.name, 'AbortError');
8586
server.close();
8687
}));
88+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
8789
assert.strictEqual(req.aborted, false);
8890
assert.strictEqual(req.destroyed, false);
8991
controller.abort();
9092
assert.strictEqual(req.aborted, false);
9193
assert.strictEqual(req.destroyed, true);
9294
}));
9395
}
96+
97+
{
98+
// Use pre-aborted AbortSignal
99+
const server = http.createServer(common.mustNotCall());
100+
const controller = new AbortController();
101+
const { signal } = controller;
102+
server.listen(0, common.mustCall(() => {
103+
controller.abort();
104+
const options = { port: server.address().port, signal };
105+
const req = http.get(options, common.mustNotCall());
106+
req.on('error', common.mustCall((err) => {
107+
assert.strictEqual(err.code, 'ABORT_ERR');
108+
assert.strictEqual(err.name, 'AbortError');
109+
server.close();
110+
}));
111+
assert.strictEqual(req.aborted, false);
112+
assert.strictEqual(req.destroyed, true);
113+
}));
114+
}

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

+46-8
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ if (!common.hasCrypto)
88
const assert = require('assert');
99
const h2 = require('http2');
1010
const { kSocket } = require('internal/http2/util');
11-
const { kEvents } = require('internal/event_target');
1211
const Countdown = require('../common/countdown');
13-
12+
const { getEventListeners } = require('events');
1413
{
1514
const server = h2.createServer();
1615
server.listen(0, common.mustCall(() => {
@@ -180,11 +179,11 @@ const Countdown = require('../common/countdown');
180179
client.on('close', common.mustCall());
181180

182181
const { signal } = controller;
183-
assert.strictEqual(signal[kEvents].get('abort'), undefined);
182+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
184183

185184
client.on('error', common.mustCall(() => {
186185
// After underlying stream dies, signal listener detached
187-
assert.strictEqual(signal[kEvents].get('abort'), undefined);
186+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
188187
}));
189188

190189
const req = client.request({}, { signal });
@@ -198,7 +197,7 @@ const Countdown = require('../common/countdown');
198197
assert.strictEqual(req.aborted, false);
199198
assert.strictEqual(req.destroyed, false);
200199
// Signal listener attached
201-
assert.strictEqual(signal[kEvents].get('abort').size, 1);
200+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
202201

203202
controller.abort();
204203

@@ -219,16 +218,16 @@ const Countdown = require('../common/countdown');
219218
const { signal } = controller;
220219
controller.abort();
221220

222-
assert.strictEqual(signal[kEvents].get('abort'), undefined);
221+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
223222

224223
client.on('error', common.mustCall(() => {
225224
// After underlying stream dies, signal listener detached
226-
assert.strictEqual(signal[kEvents].get('abort'), undefined);
225+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
227226
}));
228227

229228
const req = client.request({}, { signal });
230229
// Signal already aborted, so no event listener attached.
231-
assert.strictEqual(signal[kEvents].get('abort'), undefined);
230+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
232231

233232
assert.strictEqual(req.aborted, false);
234233
// Destroyed on same tick as request made
@@ -241,3 +240,42 @@ const Countdown = require('../common/countdown');
241240
req.on('close', common.mustCall(() => server.close()));
242241
}));
243242
}
243+
244+
// Destroy ClientHttpSession with AbortSignal
245+
{
246+
const server = h2.createServer();
247+
const controller = new AbortController();
248+
249+
server.on('stream', common.mustNotCall());
250+
server.listen(0, common.mustCall(() => {
251+
const { signal } = controller;
252+
const client = h2.connect(`http://localhost:${server.address().port}`, {
253+
signal,
254+
});
255+
client.on('close', common.mustCall());
256+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
257+
258+
client.on('error', common.mustCall(common.mustCall((err) => {
259+
assert.strictEqual(err.code, 'ABORT_ERR');
260+
assert.strictEqual(err.name, 'AbortError');
261+
})));
262+
263+
const req = client.request({}, {});
264+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
265+
266+
req.on('error', common.mustCall((err) => {
267+
assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_CANCEL');
268+
assert.strictEqual(err.name, 'Error');
269+
assert.strictEqual(req.aborted, false);
270+
assert.strictEqual(req.destroyed, true);
271+
}));
272+
req.on('close', common.mustCall(() => server.close()));
273+
274+
assert.strictEqual(req.aborted, false);
275+
assert.strictEqual(req.destroyed, false);
276+
// Signal listener attached
277+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
278+
279+
controller.abort();
280+
}));
281+
}

test/parallel/test-https-abortcontroller.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ if (!common.hasCrypto)
77
const fixtures = require('../common/fixtures');
88
const https = require('https');
99
const assert = require('assert');
10-
const { once } = require('events');
10+
const { once, getEventListeners } = require('events');
1111

1212
const options = {
1313
key: fixtures.readKey('agent1-key.pem'),
@@ -29,6 +29,7 @@ const options = {
2929
rejectUnauthorized: false,
3030
signal: ac.signal,
3131
});
32+
assert.strictEqual(getEventListeners(ac.signal, 'abort').length, 1);
3233
process.nextTick(() => ac.abort());
3334
const [ err ] = await once(req, 'error');
3435
assert.strictEqual(err.name, 'AbortError');

0 commit comments

Comments
 (0)