Skip to content

Commit 6ab2848

Browse files
committed
http: reset parser.incoming when server request is finished
This resolves a memory leak for keep-alive connections and does not regress in the way that 779a05d did by waiting for the incoming request to be finished before releasing the `parser.incoming` object. Refs: #28646 Refs: #29263 Fixes: #9668 PR-URL: #29297 Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 698a294 commit 6ab2848

File tree

3 files changed

+73
-3
lines changed

3 files changed

+73
-3
lines changed

lib/_http_server.js

+14
Original file line numberDiff line numberDiff line change
@@ -611,13 +611,27 @@ function onParserExecuteCommon(server, socket, parser, state, ret, d) {
611611
}
612612
}
613613

614+
function clearIncoming(req) {
615+
req = req || this;
616+
const parser = req.socket && req.socket.parser;
617+
// Reset the .incoming property so that the request object can be gc'ed.
618+
if (parser && parser.incoming === req) {
619+
if (req.readableEnded) {
620+
parser.incoming = null;
621+
} else {
622+
req.on('end', clearIncoming);
623+
}
624+
}
625+
}
626+
614627
function resOnFinish(req, res, socket, state, server) {
615628
// Usually the first incoming element should be our request. it may
616629
// be that in the case abortIncoming() was called that the incoming
617630
// array will be empty.
618631
assert(state.incoming.length === 0 || state.incoming[0] === req);
619632

620633
state.incoming.shift();
634+
clearIncoming(req);
621635

622636
// If the user never called req.read(), and didn't pipe() or
623637
// .resume() or .on('data'), then we call req._dump() so that the

test/parallel/test-http-server-keepalive-end.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
'use strict';
2-
32
const common = require('../common');
3+
const assert = require('assert');
44
const { createServer } = require('http');
55
const { connect } = require('net');
66

7+
// Make sure that for HTTP keepalive requests, the .on('end') event is emitted
8+
// on the incoming request object, and that the parser instance does not hold
9+
// on to that request object afterwards.
10+
711
const server = createServer(common.mustCall((req, res) => {
8-
req.on('end', common.mustCall());
12+
req.on('end', common.mustCall(() => {
13+
const parser = req.socket.parser;
14+
assert.strictEqual(parser.incoming, req);
15+
process.nextTick(() => {
16+
assert.strictEqual(parser.incoming, null);
17+
});
18+
}));
919
res.end('hello world');
1020
}));
1121

1222
server.unref();
1323

1424
server.listen(0, common.mustCall(() => {
15-
1625
const client = connect(server.address().port);
1726

1827
const req = [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Flags: --expose-gc
2+
'use strict';
3+
const common = require('../common');
4+
const onGC = require('../common/ongc');
5+
const { createServer } = require('http');
6+
const { connect } = require('net');
7+
8+
if (common.isWindows) {
9+
// TODO(addaleax): Investigate why and remove the skip.
10+
common.skip('This test is flaky on Windows.');
11+
}
12+
13+
// Make sure that for HTTP keepalive requests, the req object can be
14+
// garbage collected once the request is finished.
15+
// Refs: https://github.com/nodejs/node/issues/9668
16+
17+
let client;
18+
const server = createServer(common.mustCall((req, res) => {
19+
onGC(req, { ongc: common.mustCall() });
20+
req.resume();
21+
req.on('end', common.mustCall(() => {
22+
setImmediate(() => {
23+
client.end();
24+
global.gc();
25+
});
26+
}));
27+
res.end('hello world');
28+
}));
29+
30+
server.unref();
31+
32+
server.listen(0, common.mustCall(() => {
33+
client = connect(server.address().port);
34+
35+
const req = [
36+
'POST / HTTP/1.1',
37+
`Host: localhost:${server.address().port}`,
38+
'Connection: keep-alive',
39+
'Content-Length: 11',
40+
'',
41+
'hello world',
42+
''
43+
].join('\r\n');
44+
45+
client.write(req);
46+
client.unref();
47+
}));

0 commit comments

Comments
 (0)