Skip to content

Commit 173d044

Browse files
committed
http: align OutgoingMessage and ClientRequest destroy
Added .destroyed property to OutgoingMessage and ClientRequest to align with streams. Fixed ClientRequest.destroy to dump res and re-use socket in agent pool aligning it with abort. PR-URL: #32148 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent de8fab9 commit 173d044

7 files changed

+155
-38
lines changed

lib/_http_client.js

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const {
2929
ObjectAssign,
3030
ObjectKeys,
3131
ObjectSetPrototypeOf,
32+
Symbol
3233
} = primordials;
3334

3435
const net = require('net');
@@ -65,6 +66,7 @@ const {
6566
} = require('internal/dtrace');
6667

6768
const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
69+
const kError = Symbol('kError');
6870

6971
function validateHost(host, name) {
7072
if (host !== null && host !== undefined && typeof host !== 'string') {
@@ -337,10 +339,19 @@ ClientRequest.prototype._implicitHeader = function _implicitHeader() {
337339
};
338340

339341
ClientRequest.prototype.abort = function abort() {
340-
if (!this.aborted) {
341-
process.nextTick(emitAbortNT, this);
342+
if (this.aborted) {
343+
return;
342344
}
343345
this.aborted = true;
346+
process.nextTick(emitAbortNT, this);
347+
this.destroy();
348+
};
349+
350+
ClientRequest.prototype.destroy = function destroy(err) {
351+
if (this.destroyed) {
352+
return;
353+
}
354+
this.destroyed = true;
344355

345356
// If we're aborting, we don't care about any more response data.
346357
if (this.res) {
@@ -350,11 +361,29 @@ ClientRequest.prototype.abort = function abort() {
350361
// In the event that we don't have a socket, we will pop out of
351362
// the request queue through handling in onSocket.
352363
if (this.socket) {
353-
// in-progress
354-
this.socket.destroy();
364+
_destroy(this, this.socket, err);
365+
} else if (err) {
366+
this[kError] = err;
355367
}
356368
};
357369

370+
function _destroy(req, socket, err) {
371+
// TODO (ronag): Check if socket was used at all (e.g. headersSent) and
372+
// re-use it in that case. `req.socket` just checks whether the socket was
373+
// assigned to the request and *might* have been used.
374+
if (!req.agent || req.socket) {
375+
socket.destroy(err);
376+
} else {
377+
socket.emit('free');
378+
if (!req.aborted && !err) {
379+
err = connResetException('socket hang up');
380+
}
381+
if (err) {
382+
req.emit('error', err);
383+
}
384+
req.emit('close');
385+
}
386+
}
358387

359388
function emitAbortNT(req) {
360389
req.emit('abort');
@@ -750,14 +779,8 @@ ClientRequest.prototype.onSocket = function onSocket(socket) {
750779
};
751780

752781
function onSocketNT(req, socket) {
753-
if (req.aborted) {
754-
// If we were aborted while waiting for a socket, skip the whole thing.
755-
if (!req.agent) {
756-
socket.destroy();
757-
} else {
758-
req.emit('close');
759-
socket.emit('free');
760-
}
782+
if (req.destroyed) {
783+
_destroy(req, socket, req[kError]);
761784
} else {
762785
tickOnSocket(req, socket);
763786
}

lib/_http_outgoing.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ function OutgoingMessage() {
9393
this.outputSize = 0;
9494

9595
this.writable = true;
96+
this.destroyed = false;
9697

9798
this._last = false;
9899
this.chunkedEncoding = false;
@@ -277,6 +278,11 @@ OutgoingMessage.prototype.setTimeout = function setTimeout(msecs, callback) {
277278
// any messages, before ever calling this. In that case, just skip
278279
// it, since something else is destroying this connection anyway.
279280
OutgoingMessage.prototype.destroy = function destroy(error) {
281+
if (this.destroyed) {
282+
return;
283+
}
284+
this.destroyed = true;
285+
280286
if (this.socket) {
281287
this.socket.destroy(error);
282288
} else {

lib/internal/streams/destroy.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,6 @@ function isRequest(stream) {
163163

164164
// Normalize destroy for legacy.
165165
function destroyer(stream, err) {
166-
// request.destroy just do .end - .abort is what we want
167166
if (isRequest(stream)) return stream.abort();
168167
if (isRequest(stream.req)) return stream.req.abort();
169168
if (typeof stream.destroy === 'function') return stream.destroy(err);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
'use strict';
2+
const common = require('../common');
3+
const http = require('http');
4+
const assert = require('assert');
5+
6+
{
7+
// abort
8+
9+
const server = http.createServer(common.mustCall((req, res) => {
10+
res.end('Hello');
11+
}));
12+
13+
server.listen(0, common.mustCall(() => {
14+
const options = { port: server.address().port };
15+
const req = http.get(options, common.mustCall((res) => {
16+
res.on('data', (data) => {
17+
req.abort();
18+
assert.strictEqual(req.aborted, true);
19+
assert.strictEqual(req.destroyed, true);
20+
server.close();
21+
});
22+
}));
23+
req.on('error', common.mustNotCall());
24+
assert.strictEqual(req.aborted, false);
25+
assert.strictEqual(req.destroyed, false);
26+
}));
27+
}
28+
29+
{
30+
// destroy + res
31+
32+
const server = http.createServer(common.mustCall((req, res) => {
33+
res.end('Hello');
34+
}));
35+
36+
server.listen(0, common.mustCall(() => {
37+
const options = { port: server.address().port };
38+
const req = http.get(options, common.mustCall((res) => {
39+
res.on('data', (data) => {
40+
req.destroy();
41+
assert.strictEqual(req.aborted, false);
42+
assert.strictEqual(req.destroyed, true);
43+
server.close();
44+
});
45+
}));
46+
req.on('error', common.mustNotCall());
47+
assert.strictEqual(req.aborted, false);
48+
assert.strictEqual(req.destroyed, false);
49+
}));
50+
}
51+
52+
{
53+
// destroy
54+
55+
const server = http.createServer(common.mustNotCall((req, res) => {
56+
}));
57+
58+
server.listen(0, common.mustCall(() => {
59+
const options = { port: server.address().port };
60+
const req = http.get(options, common.mustNotCall());
61+
req.on('error', common.mustCall((err) => {
62+
assert.strictEqual(err.code, 'ECONNRESET');
63+
server.close();
64+
}));
65+
assert.strictEqual(req.aborted, false);
66+
assert.strictEqual(req.destroyed, false);
67+
req.destroy();
68+
assert.strictEqual(req.aborted, false);
69+
assert.strictEqual(req.destroyed, true);
70+
}));
71+
}

test/parallel/test-http-client-abort-keep-alive-queued-tcp-socket.js

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,45 @@ const common = require('../common');
33
const assert = require('assert');
44
const http = require('http');
55

6-
let socketsCreated = 0;
76

8-
class Agent extends http.Agent {
9-
createConnection(options, oncreate) {
10-
const socket = super.createConnection(options, oncreate);
11-
socketsCreated++;
12-
return socket;
7+
for (const destroyer of ['destroy', 'abort']) {
8+
let socketsCreated = 0;
9+
10+
class Agent extends http.Agent {
11+
createConnection(options, oncreate) {
12+
const socket = super.createConnection(options, oncreate);
13+
socketsCreated++;
14+
return socket;
15+
}
1316
}
14-
}
1517

16-
const server = http.createServer((req, res) => res.end());
18+
const server = http.createServer((req, res) => res.end());
1719

18-
server.listen(0, common.mustCall(() => {
19-
const port = server.address().port;
20-
const agent = new Agent({
21-
keepAlive: true,
22-
maxSockets: 1
23-
});
20+
server.listen(0, common.mustCall(() => {
21+
const port = server.address().port;
22+
const agent = new Agent({
23+
keepAlive: true,
24+
maxSockets: 1
25+
});
2426

25-
http.get({ agent, port }, (res) => res.resume());
27+
http.get({ agent, port }, (res) => res.resume());
2628

27-
const req = http.get({ agent, port }, common.mustNotCall());
28-
req.abort();
29+
const req = http.get({ agent, port }, common.mustNotCall());
30+
req[destroyer]();
2931

30-
http.get({ agent, port }, common.mustCall((res) => {
31-
res.resume();
32-
assert.strictEqual(socketsCreated, 1);
33-
agent.destroy();
34-
server.close();
32+
if (destroyer === 'destroy') {
33+
req.on('error', common.mustCall((err) => {
34+
assert.strictEqual(err.code, 'ECONNRESET');
35+
}));
36+
} else {
37+
req.on('error', common.mustNotCall());
38+
}
39+
40+
http.get({ agent, port }, common.mustCall((res) => {
41+
res.resume();
42+
assert.strictEqual(socketsCreated, 1);
43+
agent.destroy();
44+
server.close();
45+
}));
3546
}));
36-
}));
47+
}

test/parallel/test-http-client-close-event.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ server.listen(0, common.mustCall(() => {
1414
const req = http.get({ port: server.address().port }, common.mustNotCall());
1515
let errorEmitted = false;
1616

17-
req.on('error', (err) => {
17+
req.on('error', common.mustCall((err) => {
1818
errorEmitted = true;
1919
assert.strictEqual(err.constructor, Error);
2020
assert.strictEqual(err.message, 'socket hang up');
2121
assert.strictEqual(err.code, 'ECONNRESET');
22-
});
22+
}));
2323

2424
req.on('close', common.mustCall(() => {
2525
assert.strictEqual(errorEmitted, true);

test/parallel/test-http-outgoing-proto.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,10 @@ assert.throws(() => {
122122
name: 'TypeError',
123123
message: 'Invalid character in trailer content ["404"]'
124124
});
125+
126+
{
127+
const outgoingMessage = new OutgoingMessage();
128+
assert.strictEqual(outgoingMessage.destroyed, false);
129+
outgoingMessage.destroy();
130+
assert.strictEqual(outgoingMessage.destroyed, true);
131+
}

0 commit comments

Comments
 (0)