Skip to content

Commit f23b3b6

Browse files
tshemsedinovrvagg
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 <timur.shemsedinov@gmail.com> Author: Alexey Orlenko <eaglexrlnk@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com> Reviewed-By: Refael Ackermann <refack@gmail.com>
1 parent d82f8c4 commit f23b3b6

File tree

6 files changed

+249
-7
lines changed

6 files changed

+249
-7
lines changed

doc/api/http.md

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -873,17 +873,33 @@ customised.
873873
added: v0.9.12
874874
-->
875875

876-
* {Number} Default = 120000 (2 minutes)
876+
* {number} Timeout in milliseconds. Defaults to 120000 (2 minutes).
877877

878878
The number of milliseconds of inactivity before a socket is presumed
879879
to have timed out.
880880

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

885-
Set to 0 to disable any kind of automatic timeout behavior on incoming
886-
connections.
883+
*Note*: The socket timeout logic is set up on connection, so changing this
884+
value only affects new connections to the server, not any existing connections.
885+
886+
### server.keepAliveTimeout
887+
<!-- YAML
888+
added: REPLACEME
889+
-->
890+
891+
* {number} Timeout in milliseconds. Defaults to 5000 (5 seconds).
892+
893+
The number of milliseconds of inactivity a server needs to wait for additional
894+
incoming data, after it has finished writing the last response, before a socket
895+
will be destroyed. If the server receives new data before the keep-alive
896+
timeout has fired, it will reset the regular inactivity timeout, i.e.,
897+
[`server.timeout`][].
898+
899+
A value of 0 will disable the keep-alive timeout behavior on incoming connections.
900+
901+
*Note*: The socket timeout logic is set up on connection, so changing this
902+
value only affects new connections to the server, not any existing connections.
887903

888904
## Class: http.ServerResponse
889905
<!-- YAML
@@ -1739,6 +1755,7 @@ There are a few special headers that should be noted.
17391755
[`response.write(data, encoding)`]: #http_response_write_chunk_encoding_callback
17401756
[`response.writeContinue()`]: #http_response_writecontinue
17411757
[`response.writeHead()`]: #http_response_writehead_statuscode_statusmessage_headers
1758+
[`server.timeout`]: #http_server_timeout
17421759
[`socket.setKeepAlive()`]: net.html#net_socket_setkeepalive_enable_initialdelay
17431760
[`socket.setNoDelay()`]: net.html#net_socket_setnodelay_nodelay
17441761
[`socket.setTimeout()`]: net.html#net_socket_settimeout_timeout_callback

doc/api/https.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ added: v0.11.2
4646

4747
See [`http.Server#timeout`][].
4848

49+
### server.keepAliveTimeout
50+
<!-- YAML
51+
added: REPLACEME
52+
-->
53+
- {number} Defaults to 5000 (5 seconds).
54+
55+
See [`http.Server#keepAliveTimeout`][].
56+
4957
## https.createServer(options[, requestListener])
5058
<!-- YAML
5159
added: v0.3.4
@@ -240,6 +248,7 @@ const req = https.request(options, (res) => {
240248
[`globalAgent`]: #https_https_globalagent
241249
[`http.Agent`]: http.html#http_class_http_agent
242250
[`http.Server#headersTimeout`]: http.html#http_server_headerstimeout
251+
[`http.Server#keepAliveTimeout`]: http.html#http_server_keepalivetimeout
243252
[`http.close()`]: http.html#http_server_close_callback
244253
[`http.get()`]: http.html#http_http_get_options_callback
245254
[`http.listen()`]: http.html#http_server_listen_port_hostname_backlog_callback

lib/_http_server.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ function Server(requestListener) {
243243
this.addListener('connection', connectionListener);
244244

245245
this.timeout = 2 * 60 * 1000;
246+
this.keepAliveTimeout = 5000;
246247

247248
this._pendingResponseData = 0;
248249
this.headersTimeout = 40 * 1000; // 40 seconds
@@ -316,6 +317,8 @@ function connectionListener(socket) {
316317
socket.destroy();
317318
});
318319

320+
socket._keepAliveTimeoutSet = false;
321+
319322
var parser = parsers.alloc();
320323
parser.reinitialize(HTTPParser.REQUEST);
321324
parser.socket = socket;
@@ -371,6 +374,15 @@ function connectionListener(socket) {
371374
function socketOnData(d) {
372375
assert(!socket._paused);
373376
debug('SERVER socketOnData %d', d.length);
377+
378+
if (socket._keepAliveTimeoutSet) {
379+
socket.setTimeout(0);
380+
if (self.timeout) {
381+
socket.setTimeout(self.timeout);
382+
}
383+
socket._keepAliveTimeoutSet = false;
384+
}
385+
374386
var ret = parser.execute(d);
375387

376388
onParserExecuteCommon(ret, d);
@@ -535,6 +547,12 @@ function connectionListener(socket) {
535547

536548
if (res._last) {
537549
socket.destroySoon();
550+
} else if (outgoing.length === 0) {
551+
if (self.keepAliveTimeout) {
552+
socket.setTimeout(0);
553+
socket.setTimeout(self.keepAliveTimeout);
554+
socket._keepAliveTimeoutSet = true;
555+
}
538556
} else {
539557
// start sending the next message
540558
var m = outgoing.shift();

lib/https.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function Server(opts, requestListener) {
3737
});
3838

3939
this.timeout = 2 * 60 * 1000;
40-
40+
this.keepAliveTimeout = 5000;
4141
this.headersTimeout = 40 * 1000; // 40 seconds
4242
}
4343
inherits(Server, tls.Server);
Lines changed: 95 additions & 0 deletions
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+
});
Lines changed: 103 additions & 0 deletions
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)