Skip to content

Commit a7f5c9c

Browse files
tshemsedinovjasnell
authored andcommitted
http: destroy sockets after keepAliveTimeout
Implement server.keepAliveTimeout in addition to server.timeout to prevent temporary socket/memory leaking in keep-alive mode. PR-URL: #2534 Author: Timur Shemsedinov <[email protected]> Author: Alexey Orlenko <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Franziska Hinkelmann <[email protected]> Reviewed-By: Refael Ackermann <[email protected]>
1 parent bb77cce commit a7f5c9c

6 files changed

+252
-11
lines changed

doc/api/http.md

+23-6
Original file line numberDiff line numberDiff line change
@@ -832,17 +832,33 @@ Returns `server`.
832832
added: v0.9.12
833833
-->
834834

835-
* {number} Defaults to 120000 (2 minutes).
835+
* {number} Timeout in milliseconds. Defaults to 120000 (2 minutes).
836836

837837
The number of milliseconds of inactivity before a socket is presumed
838838
to have timed out.
839839

840-
Note that the socket timeout logic is set up on connection, so
841-
changing this value only affects *new* connections to the server, not
842-
any existing connections.
840+
A value of 0 will disable the timeout behavior on incoming connections.
843841

844-
Set to 0 to disable any kind of automatic timeout behavior on incoming
845-
connections.
842+
*Note*: The socket timeout logic is set up on connection, so changing this
843+
value only affects new connections to the server, not any existing connections.
844+
845+
### server.keepAliveTimeout
846+
<!-- YAML
847+
added: REPLACEME
848+
-->
849+
850+
* {number} Timeout in milliseconds. Defaults to 5000 (5 seconds).
851+
852+
The number of milliseconds of inactivity a server needs to wait for additional
853+
incoming data, after it has finished writing the last response, before a socket
854+
will be destroyed. If the server receives new data before the keep-alive
855+
timeout has fired, it will reset the regular inactivity timeout, i.e.,
856+
[`server.timeout`][].
857+
858+
A value of 0 will disable the keep-alive timeout behavior on incoming connections.
859+
860+
*Note*: The socket timeout logic is set up on connection, so changing this
861+
value only affects new connections to the server, not any existing connections.
846862

847863
## Class: http.ServerResponse
848864
<!-- YAML
@@ -1764,6 +1780,7 @@ There are a few special headers that should be noted.
17641780
[`response.write(data, encoding)`]: #http_response_write_chunk_encoding_callback
17651781
[`response.writeContinue()`]: #http_response_writecontinue
17661782
[`response.writeHead()`]: #http_response_writehead_statuscode_statusmessage_headers
1783+
[`server.timeout`]: #http_server_timeout
17671784
[`socket.setKeepAlive()`]: net.html#net_socket_setkeepalive_enable_initialdelay
17681785
[`socket.setNoDelay()`]: net.html#net_socket_setnodelay_nodelay
17691786
[`socket.setTimeout()`]: net.html#net_socket_settimeout_timeout_callback

doc/api/https.md

+9
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ added: v0.11.2
3838

3939
See [`http.Server#timeout`][].
4040

41+
### server.keepAliveTimeout
42+
<!-- YAML
43+
added: REPLACEME
44+
-->
45+
- {number} Defaults to 5000 (5 seconds).
46+
47+
See [`http.Server#keepAliveTimeout`][].
48+
4149
## https.createServer(options[, requestListener])
4250
<!-- YAML
4351
added: v0.3.4
@@ -229,6 +237,7 @@ const req = https.request(options, (res) => {
229237

230238
[`Agent`]: #https_class_https_agent
231239
[`http.Agent`]: http.html#http_class_http_agent
240+
[`http.Server#keepAliveTimeout`]: http.html#http_server_keepalivetimeout
232241
[`http.Server#setTimeout()`]: http.html#http_server_settimeout_msecs_callback
233242
[`http.Server#timeout`]: http.html#http_server_timeout
234243
[`http.Server`]: http.html#http_class_http_server

lib/_http_server.js

+21-5
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ function Server(requestListener) {
271271
this.on('connection', connectionListener);
272272

273273
this.timeout = 2 * 60 * 1000;
274-
274+
this.keepAliveTimeout = 5000;
275275
this._pendingResponseData = 0;
276276
this.maxHeadersCount = null;
277277
}
@@ -323,7 +323,8 @@ function connectionListener(socket) {
323323
// inactive responses. If more data than the high watermark is queued - we
324324
// need to pause TCP socket/HTTP parser, and wait until the data will be
325325
// sent to the client.
326-
outgoingData: 0
326+
outgoingData: 0,
327+
keepAliveTimeoutSet: false
327328
};
328329
state.onData = socketOnData.bind(undefined, this, socket, parser, state);
329330
state.onEnd = socketOnEnd.bind(undefined, this, socket, parser, state);
@@ -431,8 +432,16 @@ function socketOnEnd(server, socket, parser, state) {
431432
function socketOnData(server, socket, parser, state, d) {
432433
assert(!socket._paused);
433434
debug('SERVER socketOnData %d', d.length);
434-
var ret = parser.execute(d);
435435

436+
if (state.keepAliveTimeoutSet) {
437+
socket.setTimeout(0);
438+
if (server.timeout) {
439+
socket.setTimeout(server.timeout);
440+
}
441+
state.keepAliveTimeoutSet = false;
442+
}
443+
444+
var ret = parser.execute(d);
436445
onParserExecuteCommon(server, socket, parser, state, ret, d);
437446
}
438447

@@ -496,7 +505,7 @@ function onParserExecuteCommon(server, socket, parser, state, ret, d) {
496505
}
497506
}
498507

499-
function resOnFinish(req, res, socket, state) {
508+
function resOnFinish(req, res, socket, state, server) {
500509
// Usually the first incoming element should be our request. it may
501510
// be that in the case abortIncoming() was called that the incoming
502511
// array will be empty.
@@ -514,6 +523,12 @@ function resOnFinish(req, res, socket, state) {
514523

515524
if (res._last) {
516525
socket.destroySoon();
526+
} else if (state.outgoing.length === 0) {
527+
if (server.keepAliveTimeout) {
528+
socket.setTimeout(0);
529+
socket.setTimeout(server.keepAliveTimeout);
530+
state.keepAliveTimeoutSet = true;
531+
}
517532
} else {
518533
// start sending the next message
519534
var m = state.outgoing.shift();
@@ -560,7 +575,8 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
560575

561576
// When we're finished writing the response, check if this is the last
562577
// response, if so destroy the socket.
563-
res.on('finish', resOnFinish.bind(undefined, req, res, socket, state));
578+
res.on('finish',
579+
resOnFinish.bind(undefined, req, res, socket, state, server));
564580

565581
if (req.headers.expect !== undefined &&
566582
(req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) {

lib/https.js

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function Server(opts, requestListener) {
5959
});
6060

6161
this.timeout = 2 * 60 * 1000;
62+
this.keepAliveTimeout = 5000;
6263
}
6364
inherits(Server, tls.Server);
6465
exports.Server = Server;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const http = require('http');
6+
const net = require('net');
7+
8+
const tests = [];
9+
10+
function test(fn) {
11+
if (!tests.length) {
12+
process.nextTick(run);
13+
}
14+
tests.push(fn);
15+
}
16+
17+
function run() {
18+
const fn = tests.shift();
19+
if (fn) fn(run);
20+
}
21+
22+
test(function serverEndKeepAliveTimeoutWithPipeline(cb) {
23+
let socket;
24+
let destroyedSockets = 0;
25+
let timeoutCount = 0;
26+
let requestCount = 0;
27+
process.on('exit', () => {
28+
assert.strictEqual(timeoutCount, 1);
29+
assert.strictEqual(requestCount, 3);
30+
assert.strictEqual(destroyedSockets, 1);
31+
});
32+
const server = http.createServer((req, res) => {
33+
socket = req.socket;
34+
requestCount++;
35+
res.end();
36+
});
37+
server.setTimeout(200, (socket) => {
38+
timeoutCount++;
39+
socket.destroy();
40+
});
41+
server.keepAliveTimeout = 50;
42+
server.listen(0, common.mustCall(() => {
43+
const options = {
44+
port: server.address().port,
45+
allowHalfOpen: true
46+
};
47+
const c = net.connect(options, () => {
48+
c.write('GET /1 HTTP/1.1\r\nHost: localhost\r\n\r\n');
49+
c.write('GET /2 HTTP/1.1\r\nHost: localhost\r\n\r\n');
50+
c.write('GET /3 HTTP/1.1\r\nHost: localhost\r\n\r\n');
51+
});
52+
setTimeout(() => {
53+
server.close();
54+
if (socket.destroyed) destroyedSockets++;
55+
cb();
56+
}, 1000);
57+
}));
58+
});
59+
60+
test(function serverNoEndKeepAliveTimeoutWithPipeline(cb) {
61+
let socket;
62+
let destroyedSockets = 0;
63+
let timeoutCount = 0;
64+
let requestCount = 0;
65+
process.on('exit', () => {
66+
assert.strictEqual(timeoutCount, 1);
67+
assert.strictEqual(requestCount, 3);
68+
assert.strictEqual(destroyedSockets, 1);
69+
});
70+
const server = http.createServer((req, res) => {
71+
socket = req.socket;
72+
requestCount++;
73+
});
74+
server.setTimeout(200, (socket) => {
75+
timeoutCount++;
76+
socket.destroy();
77+
});
78+
server.keepAliveTimeout = 50;
79+
server.listen(0, common.mustCall(() => {
80+
const options = {
81+
port: server.address().port,
82+
allowHalfOpen: true
83+
};
84+
const c = net.connect(options, () => {
85+
c.write('GET /1 HTTP/1.1\r\nHost: localhost\r\n\r\n');
86+
c.write('GET /2 HTTP/1.1\r\nHost: localhost\r\n\r\n');
87+
c.write('GET /3 HTTP/1.1\r\nHost: localhost\r\n\r\n');
88+
});
89+
setTimeout(() => {
90+
server.close();
91+
if (socket.destroyed) destroyedSockets++;
92+
cb();
93+
}, 1000);
94+
}));
95+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const https = require('https');
6+
const tls = require('tls');
7+
const fs = require('fs');
8+
9+
const tests = [];
10+
11+
const serverOptions = {
12+
key: fs.readFileSync(common.fixturesDir + '/keys/agent1-key.pem'),
13+
cert: fs.readFileSync(common.fixturesDir + '/keys/agent1-cert.pem')
14+
};
15+
16+
function test(fn) {
17+
if (!tests.length) {
18+
process.nextTick(run);
19+
}
20+
tests.push(fn);
21+
}
22+
23+
function run() {
24+
const fn = tests.shift();
25+
if (fn) fn(run);
26+
}
27+
28+
test(function serverKeepAliveTimeoutWithPipeline(cb) {
29+
let socket;
30+
let destroyedSockets = 0;
31+
let timeoutCount = 0;
32+
let requestCount = 0;
33+
process.on('exit', function() {
34+
assert.strictEqual(timeoutCount, 1);
35+
assert.strictEqual(requestCount, 3);
36+
assert.strictEqual(destroyedSockets, 1);
37+
});
38+
const server = https.createServer(serverOptions, (req, res) => {
39+
socket = req.socket;
40+
requestCount++;
41+
res.end();
42+
});
43+
server.setTimeout(200, (socket) => {
44+
timeoutCount++;
45+
socket.destroy();
46+
});
47+
server.keepAliveTimeout = 50;
48+
server.listen(0, common.mustCall(() => {
49+
const options = {
50+
port: server.address().port,
51+
allowHalfOpen: true,
52+
rejectUnauthorized: false
53+
};
54+
const c = tls.connect(options, () => {
55+
c.write('GET /1 HTTP/1.1\r\nHost: localhost\r\n\r\n');
56+
c.write('GET /2 HTTP/1.1\r\nHost: localhost\r\n\r\n');
57+
c.write('GET /3 HTTP/1.1\r\nHost: localhost\r\n\r\n');
58+
});
59+
setTimeout(() => {
60+
server.close();
61+
if (socket.destroyed) destroyedSockets++;
62+
cb();
63+
}, 1000);
64+
}));
65+
});
66+
67+
test(function serverNoEndKeepAliveTimeoutWithPipeline(cb) {
68+
let socket;
69+
let destroyedSockets = 0;
70+
let timeoutCount = 0;
71+
let requestCount = 0;
72+
process.on('exit', () => {
73+
assert.strictEqual(timeoutCount, 1);
74+
assert.strictEqual(requestCount, 3);
75+
assert.strictEqual(destroyedSockets, 1);
76+
});
77+
const server = https.createServer(serverOptions, (req, res) => {
78+
socket = req.socket;
79+
requestCount++;
80+
});
81+
server.setTimeout(200, (socket) => {
82+
timeoutCount++;
83+
socket.destroy();
84+
});
85+
server.keepAliveTimeout = 50;
86+
server.listen(0, common.mustCall(() => {
87+
const options = {
88+
port: server.address().port,
89+
allowHalfOpen: true,
90+
rejectUnauthorized: false
91+
};
92+
const c = tls.connect(options, () => {
93+
c.write('GET /1 HTTP/1.1\r\nHost: localhost\r\n\r\n');
94+
c.write('GET /2 HTTP/1.1\r\nHost: localhost\r\n\r\n');
95+
c.write('GET /3 HTTP/1.1\r\nHost: localhost\r\n\r\n');
96+
});
97+
setTimeout(() => {
98+
server.close();
99+
if (socket && socket.destroyed) destroyedSockets++;
100+
cb();
101+
}, 1000);
102+
}));
103+
});

0 commit comments

Comments
 (0)