Skip to content

Commit b27b85f

Browse files
ronagjuanarbol
authored andcommitted
http: cleanup pipeline queue
When a socket with pipelined requests is destroyed then some requests will leak. PR-URL: #62534 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
1 parent 7f78de5 commit b27b85f

4 files changed

Lines changed: 92 additions & 4 deletions

File tree

lib/_http_outgoing.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -386,14 +386,19 @@ OutgoingMessage.prototype.destroy = function destroy(error) {
386386
if (this[kSocket]) {
387387
this[kSocket].destroy(error);
388388
} else {
389-
this.once('socket', function socketDestroyOnConnect(socket) {
390-
socket.destroy(error);
391-
});
389+
process.nextTick(emitDestroyNT, this);
392390
}
393391

394392
return this;
395393
};
396394

395+
function emitDestroyNT(self) {
396+
if (!self._closed) {
397+
self._closed = true;
398+
self.emit('close');
399+
}
400+
}
401+
397402

398403
// This abstract either writing directly to the socket or buffering it.
399404
OutgoingMessage.prototype._send = function _send(data, encoding, callback, byteLength) {

lib/_http_server.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -833,14 +833,21 @@ function socketOnClose(socket, state) {
833833
debug('server socket close');
834834
freeParser(socket.parser, null, socket);
835835
abortIncoming(state.incoming);
836+
abortOutgoing(state.outgoing);
836837
}
837838

838839
function abortIncoming(incoming) {
839840
while (incoming.length) {
840841
const req = incoming.shift();
841842
req.destroy(new ConnResetException('aborted'));
842843
}
843-
// Abort socket._httpMessage ?
844+
}
845+
846+
function abortOutgoing(outgoing) {
847+
while (outgoing.length) {
848+
const req = outgoing.shift();
849+
req.destroy(new ConnResetException('aborted'));
850+
}
844851
}
845852

846853
function socketOnEnd(server, socket, parser, state) {

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,32 @@
22
const common = require('../common');
33
const http = require('http');
44
const assert = require('assert');
5+
const { OutgoingMessage } = require('http');
6+
7+
// OutgoingMessage.destroy() with no socket should emit 'close' and set closed.
8+
{
9+
const msg = new OutgoingMessage();
10+
assert.strictEqual(msg.destroyed, false);
11+
assert.strictEqual(msg.closed, false);
12+
msg.on('close', common.mustCall(() => {
13+
assert.strictEqual(msg.destroyed, true);
14+
assert.strictEqual(msg.closed, true);
15+
}));
16+
msg.destroy();
17+
assert.strictEqual(msg.destroyed, true);
18+
}
19+
20+
// OutgoingMessage.destroy(err) with no socket should set errored and emit 'close'.
21+
{
22+
const msg = new OutgoingMessage();
23+
const err = new Error('test destroy');
24+
msg.on('close', common.mustCall(() => {
25+
assert.strictEqual(msg.closed, true);
26+
assert.strictEqual(msg.errored, err);
27+
}));
28+
msg.destroy(err);
29+
assert.strictEqual(msg.errored, err);
30+
}
531

632
{
733
const server = http.createServer(common.mustCall((req, res) => {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
// Test that queued (pipelined) outgoing responses are destroyed when the
3+
// socket closes before the first response has finished. Previously,
4+
// socketOnClose only aborted state.incoming (pending requests) but left
5+
// state.outgoing responses with socket=null alive forever.
6+
7+
const common = require('../common');
8+
const http = require('http');
9+
const net = require('net');
10+
const assert = require('assert');
11+
12+
let requestCount = 0;
13+
14+
const server = http.createServer(common.mustCall((req, res) => {
15+
requestCount++;
16+
17+
if (requestCount === 1) {
18+
// Keep the first response open so the second response is queued in
19+
// state.outgoing with socket === null.
20+
res.writeHead(200);
21+
res.write('start');
22+
// Intentionally do not call res.end().
23+
} else {
24+
// The second response should be queued — no socket assigned yet.
25+
assert.strictEqual(res.socket, null);
26+
assert.strictEqual(res.destroyed, false);
27+
assert.strictEqual(res.closed, false);
28+
29+
res.on('close', common.mustCall(() => {
30+
assert.strictEqual(res.destroyed, true);
31+
assert.strictEqual(res.closed, true);
32+
server.close();
33+
}));
34+
35+
// Simulate client dying while first response is still in flight.
36+
req.socket.destroy();
37+
}
38+
}, 2));
39+
40+
server.listen(0, common.mustCall(function() {
41+
const port = this.address().port;
42+
const client = net.connect(port);
43+
44+
// Send two pipelined HTTP/1.1 requests at once.
45+
client.write(
46+
`GET /1 HTTP/1.1\r\nHost: localhost:${port}\r\n\r\n` +
47+
`GET /2 HTTP/1.1\r\nHost: localhost:${port}\r\n\r\n`,
48+
);
49+
client.resume();
50+
}));

0 commit comments

Comments
 (0)