Skip to content

Commit 23318c7

Browse files
aqrlnMylesBorins
authored andcommitted
http: fix timeout reset after keep-alive timeout
Fix the logic of resetting the socket timeout of keep-alive HTTP connections and add two tests: * `test-http-server-keep-alive-timeout-slow-server` is a regression test for GH-13391. It ensures that the server-side keep-alive timeout will not fire during processing of a request. * `test-http-server-keep-alive-timeout-slow-client-headers` ensures that the regular socket timeout is restored as soon as a client starts sending a new request, not as soon as the whole message is received, so that the keep-alive timeout will not fire while, e.g., the client is sending large cookies. Refs: #2534 Fixes: #13391 PR-URL: #13549 Reviewed-By: Refael Ackermann <refack@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Brian White <mscdex@mscdex.net>
1 parent 2ea529b commit 23318c7

File tree

3 files changed

+119
-8
lines changed

3 files changed

+119
-8
lines changed

lib/_http_server.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -433,14 +433,6 @@ function socketOnData(server, socket, parser, state, d) {
433433
assert(!socket._paused);
434434
debug('SERVER socketOnData %d', d.length);
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-
444436
var ret = parser.execute(d);
445437
onParserExecuteCommon(server, socket, parser, state, ret, d);
446438
}
@@ -461,6 +453,8 @@ function socketOnError(e) {
461453
}
462454

463455
function onParserExecuteCommon(server, socket, parser, state, ret, d) {
456+
resetSocketTimeout(server, socket, state);
457+
464458
if (ret instanceof Error) {
465459
debug('parse error', ret);
466460
socketOnError.call(socket, ret);
@@ -542,6 +536,8 @@ function resOnFinish(req, res, socket, state, server) {
542536
// new message. In this callback we setup the response object and pass it
543537
// to the user.
544538
function parserOnIncoming(server, socket, state, req, keepAlive) {
539+
resetSocketTimeout(server, socket, state);
540+
545541
state.incoming.push(req);
546542

547543
// If the writable end isn't consuming, then stop reading
@@ -603,6 +599,14 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
603599
return false; // Not a HEAD response. (Not even a response!)
604600
}
605601

602+
function resetSocketTimeout(server, socket, state) {
603+
if (!state.keepAliveTimeoutSet)
604+
return;
605+
606+
socket.setTimeout(server.timeout || 0);
607+
state.keepAliveTimeoutSet = false;
608+
}
609+
606610
function onSocketResume() {
607611
// It may seem that the socket is resumed, but this is an enemy's trick to
608612
// deceive us! `resume` is emitted asynchronously, and may be called from
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 server = http.createServer(common.mustCall((req, res) => {
9+
res.end();
10+
}, 2));
11+
12+
server.keepAliveTimeout = common.platformTimeout(100);
13+
14+
server.listen(0, common.mustCall(() => {
15+
const port = server.address().port;
16+
const socket = net.connect({ port }, common.mustCall(() => {
17+
request(common.mustCall(() => {
18+
// Make a second request on the same socket, after the keep-alive timeout
19+
// has been set on the server side.
20+
request(common.mustCall());
21+
}));
22+
}));
23+
24+
server.on('timeout', common.mustCall(() => {
25+
socket.end();
26+
server.close();
27+
}));
28+
29+
function request(callback) {
30+
socket.setEncoding('utf8');
31+
socket.on('data', onData);
32+
let response = '';
33+
34+
// Simulate a client that sends headers slowly (with a period of inactivity
35+
// that is longer than the keep-alive timeout).
36+
socket.write('GET / HTTP/1.1\r\n' +
37+
`Host: localhost:${port}\r\n`);
38+
setTimeout(() => {
39+
socket.write('Connection: keep-alive\r\n' +
40+
'\r\n');
41+
}, common.platformTimeout(300));
42+
43+
function onData(chunk) {
44+
response += chunk;
45+
if (chunk.includes('\r\n')) {
46+
socket.removeListener('data', onData);
47+
onHeaders();
48+
}
49+
}
50+
51+
function onHeaders() {
52+
assert.ok(response.includes('HTTP/1.1 200 OK\r\n'));
53+
assert.ok(response.includes('Connection: keep-alive\r\n'));
54+
callback();
55+
}
56+
}
57+
}));
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const http = require('http');
6+
7+
const server = http.createServer(common.mustCall((req, res) => {
8+
if (req.url === '/first') {
9+
res.end('ok');
10+
return;
11+
}
12+
setTimeout(() => {
13+
res.end('ok');
14+
}, common.platformTimeout(500));
15+
}, 2));
16+
17+
server.keepAliveTimeout = common.platformTimeout(200);
18+
19+
const agent = new http.Agent({
20+
keepAlive: true,
21+
maxSockets: 1
22+
});
23+
24+
function request(path, callback) {
25+
const port = server.address().port;
26+
const req = http.request({ agent, path, port }, common.mustCall((res) => {
27+
assert.strictEqual(res.statusCode, 200);
28+
29+
res.setEncoding('utf8');
30+
31+
let result = '';
32+
res.on('data', (chunk) => {
33+
result += chunk;
34+
});
35+
36+
res.on('end', common.mustCall(() => {
37+
assert.strictEqual(result, 'ok');
38+
callback();
39+
}));
40+
}));
41+
req.end();
42+
}
43+
44+
server.listen(0, common.mustCall(() => {
45+
request('/first', () => {
46+
request('/second', () => {
47+
server.close();
48+
});
49+
});
50+
}));

0 commit comments

Comments
 (0)