Skip to content

Commit ea23024

Browse files
committed
http: allow to close connections on server.close
1 parent c65746e commit ea23024

8 files changed

+288
-8
lines changed

doc/api/http.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1443,16 +1443,31 @@ This event is guaranteed to be passed an instance of the {net.Socket} class,
14431443
a subclass of {stream.Duplex}, unless the user specifies a socket
14441444
type other than {net.Socket}.
14451445

1446-
### `server.close([callback])`
1446+
### `server.close([options][, callback])`
14471447

14481448
<!-- YAML
14491449
added: v0.1.90
1450+
changes:
1451+
- version: REPLACEME
1452+
pr-url: https://github.com/nodejs/node/pull/42811
1453+
description: The `options` argument is now supported.
14501454
-->
14511455

1456+
* `options` {Object}
1457+
* `force` {string} Specified which kind of already opened connections to
1458+
forcefully close. Supported values are:
1459+
* `keep-alive`: Will close all idle connections (which are using
1460+
HTTP Keep-Alive).
1461+
* `all`: Will close all currently opened connections without completing
1462+
any request.
14521463
* `callback` {Function}
14531464

14541465
Stops the server from accepting new connections. See [`net.Server.close()`][].
14551466

1467+
If no `options.force` is specified (the default), the server will not accept
1468+
new connections but will complete pending requests and it will serve new
1469+
requests received through already opened connections using HTTP Keep-Alive.
1470+
14561471
### `server.headersTimeout`
14571472

14581473
<!-- YAML

lib/_http_server.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,9 +453,36 @@ function Server(options, requestListener) {
453453
ObjectSetPrototypeOf(Server.prototype, net.Server.prototype);
454454
ObjectSetPrototypeOf(Server, net.Server);
455455

456-
Server.prototype.close = function() {
456+
Server.prototype.close = function(args, callback) {
457457
clearInterval(this[kConnectionsCheckingInterval]);
458-
ReflectApply(net.Server.prototype.close, this, arguments);
458+
459+
if (typeof args === 'function') {
460+
callback = args
461+
args = {}
462+
}
463+
464+
ReflectApply(net.Server.prototype.close, this, [callback]);
465+
466+
if (typeof args === 'object' && 'force' in args) {
467+
let toClose = []
468+
469+
switch (args.force) {
470+
case 'keep-alive':
471+
toClose = this[kConnections].idle();
472+
break;
473+
case 'all':
474+
toClose = this[kConnections].all();
475+
break;
476+
}
477+
478+
for (let i = 0, length = toClose.length; i < length; i++) {
479+
const socket = toClose[i].socket;
480+
481+
if (socket) {
482+
socket.destroy();
483+
}
484+
}
485+
}
459486
};
460487

461488
Server.prototype.setTimeout = function setTimeout(msecs, callback) {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
5+
const { createServer } = require('http');
6+
const { connect } = require('net');
7+
8+
const server = createServer(common.mustCall(function(req, res) {
9+
res.writeHead(200, { 'Connection': 'keep-alive' });
10+
res.end();
11+
}), { requestTimeout: 60000, headersTimeout: 0 });
12+
13+
14+
server.listen(0, function() {
15+
const port = server.address().port;
16+
17+
// Create a new request, let it finish but leave the connection opened using HTTP keep-alive
18+
const client1 = connect(port);
19+
let response1 = '';
20+
21+
client1.on('data', common.mustCall((chunk) => {
22+
response1 += chunk.toString('utf-8');
23+
}));
24+
25+
client1.on('end', common.mustCall(function() {
26+
assert(response1.startsWith('HTTP/1.1 200 OK\r\nConnection: keep-alive'));
27+
}));
28+
client1.on('close', common.mustCall());
29+
30+
client1.resume();
31+
client1.write('GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n');
32+
33+
// Create a second request but never finish it
34+
const client2 = connect(port);
35+
client2.on('close', common.mustCall());
36+
37+
client2.resume();
38+
client2.write('GET / HTTP/1.1\r\n');
39+
40+
// Now, after a while, close the server
41+
setTimeout(() => {
42+
server.close({ force: 'all' }, common.mustCall());
43+
}, common.platformTimeout(1000));
44+
45+
// This timer should never go off as the server.close should shut everything down
46+
setTimeout(common.mustNotCall(), common.platformTimeout(1500)).unref();
47+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
5+
const { createServer } = require('http');
6+
const { connect } = require('net');
7+
8+
const server = createServer(common.mustCall(function(req, res) {
9+
res.writeHead(200, { 'Connection': 'keep-alive' });
10+
res.end();
11+
}), { requestTimeout: 60000, headersTimeout: 0 });
12+
13+
14+
server.listen(0, function() {
15+
const port = server.address().port;
16+
17+
// Create a new request, let it finish but leave the connection opened using HTTP keep-alive
18+
const client1 = connect(port);
19+
let response1 = '';
20+
let client1Closed = false;
21+
22+
client1.on('data', common.mustCall((chunk) => {
23+
response1 += chunk.toString('utf-8');
24+
}));
25+
26+
client1.on('end', common.mustCall(function() {
27+
assert(response1.startsWith('HTTP/1.1 200 OK\r\nConnection: keep-alive'));
28+
}));
29+
client1.on('close', common.mustCall(() => {
30+
client1Closed = true;
31+
}));
32+
33+
client1.resume();
34+
client1.write('GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n');
35+
36+
// Create a second request but never finish it
37+
const client2 = connect(port);
38+
let client2Closed = false;
39+
client2.on('close', common.mustCall(() => {
40+
client2Closed = true;
41+
}));
42+
43+
client2.resume();
44+
client2.write('GET / HTTP/1.1\r\n');
45+
46+
// Now, after a while, close the server
47+
setTimeout(() => {
48+
server.close({ force: 'keep-alive' }, common.mustCall());
49+
}, common.platformTimeout(1000));
50+
51+
// Check that only the idle connection got closed
52+
setTimeout(common.mustCall(() => {
53+
assert(client1Closed);
54+
assert(!client2Closed);
55+
server.close({ force: 'all' });
56+
}), common.platformTimeout(1500)).unref();
57+
});

test/parallel/test-http-server-request-timeout-keepalive.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ function performRequestWithDelay(client, firstDelay, secondDelay, closeAfter) {
2828
}
2929

3030
const requestTimeout = common.platformTimeout(1000);
31+
const connectionsCheckingInterval = common.platformTimeout(250)
3132
const server = createServer({
3233
headersTimeout: 0,
3334
requestTimeout,
3435
keepAliveTimeout: 0,
35-
connectionsCheckingInterval: common.platformTimeout(250),
36+
connectionsCheckingInterval,
3637
}, common.mustCallAtLeast((req, res) => {
3738
res.writeHead(200, { 'Content-Type': 'text/plain' });
3839
res.end();
@@ -70,7 +71,7 @@ server.listen(0, common.mustCall(() => {
7071
performRequestWithDelay(
7172
client,
7273
requestTimeout / 5,
73-
requestTimeout,
74+
requestTimeout * 2,
7475
true
7576
);
7677
}, defer).unref();
@@ -91,7 +92,7 @@ server.listen(0, common.mustCall(() => {
9192
client.on('error', errOrEnd);
9293
client.on('end', errOrEnd);
9394

94-
// Perform a second request expected to finish before requestTimeout
95+
// Perform a first request which is completed immediately
9596
performRequestWithDelay(
9697
client,
9798
requestTimeout / 5,

test/parallel/test-http-server-request-timeout-pipelining.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,5 @@ server.listen(0, common.mustCall(() => {
6666
// Complete the request
6767
setTimeout(() => {
6868
client.write('close\r\n\r\n');
69-
}, requestTimeout * 1.5).unref();
69+
}, requestTimeout * 2).unref();
7070
}));

test/parallel/test-http-server-request-timeout-upgrade.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,6 @@ server.listen(0, common.mustCall(() => {
5858
setTimeout(() => {
5959
client.write('12345678901234567890');
6060
client.end();
61-
}, common.platformTimeout(2000)).unref();
61+
}, common.platformTimeout(requestTimeout * 2)).unref();
6262
});
6363
}));
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { createServer } = require('http');
6+
const { connect } = require('net');
7+
8+
// This test validates that request are correct checked for both requests and headers timeout in various situations.
9+
10+
const requestBodyPart1 = 'POST / HTTP/1.1\r\nContent-Length: 20\r\n';
11+
const requestBodyPart2 = 'Connection: close\r\n\r\n1234567890';
12+
const requestBodyPart3 = '1234567890';
13+
14+
const responseOk = 'HTTP/1.1 200 OK\r\n';
15+
const responseTimeout = 'HTTP/1.1 408 Request Timeout\r\n';
16+
17+
const headersTimeout = common.platformTimeout(2000);
18+
const connectionsCheckingInterval = common.platformTimeout(500);
19+
20+
const server = createServer({
21+
headersTimeout,
22+
requestTimeout: headersTimeout * 2,
23+
keepAliveTimeout: 0,
24+
connectionsCheckingInterval,
25+
}, common.mustCall((req, res) => {
26+
req.on('data', () => {
27+
// No-op
28+
});
29+
30+
req.on('end', () => {
31+
res.writeHead(200, { 'Content-Type': 'text/plain' });
32+
res.end();
33+
});
34+
}, 4));
35+
36+
assert.strictEqual(server.headersTimeout, headersTimeout);
37+
assert.strictEqual(server.requestTimeout, headersTimeout * 2);
38+
39+
let i = 0;
40+
function createClient(server) {
41+
const request = {
42+
index: i++,
43+
client: connect(server.address().port),
44+
response: '',
45+
completed: false
46+
};
47+
48+
request.client.on('data', common.mustCallAtLeast((chunk) => {
49+
request.response += chunk.toString('utf-8');
50+
}));
51+
52+
request.client.on('end', common.mustCall(() => {
53+
request.completed = true;
54+
}));
55+
56+
request.client.on('error', common.mustNotCall());
57+
58+
request.client.resume();
59+
60+
return request;
61+
}
62+
63+
server.listen(0, common.mustCall(() => {
64+
const request1 = createClient(server);
65+
let request2;
66+
let request3;
67+
let request4;
68+
let request5;
69+
70+
// Send the first request and stop before the body
71+
request1.client.write(requestBodyPart1);
72+
73+
// After a little while send two new requests
74+
setTimeout(() => {
75+
request2 = createClient(server);
76+
request3 = createClient(server);
77+
78+
// Send the second request, stop in the middle of the headers
79+
request2.client.write(requestBodyPart1);
80+
// Send the second request, stop in the middle of the headers
81+
request3.client.write(requestBodyPart1);
82+
}, headersTimeout * 0.2);
83+
84+
// After another little while send the last two new requests
85+
setTimeout(() => {
86+
request4 = createClient(server);
87+
request5 = createClient(server);
88+
89+
// Send the fourth request, stop in the middle of the headers
90+
request4.client.write(requestBodyPart1);
91+
// Send the fifth request, stop in the middle of the headers
92+
request5.client.write(requestBodyPart1);
93+
}, headersTimeout * 0.6);
94+
95+
setTimeout(() => {
96+
// Finish the first request
97+
request1.client.write(requestBodyPart2 + requestBodyPart3);
98+
99+
// Complete headers for all requests but second
100+
request3.client.write(requestBodyPart2);
101+
request4.client.write(requestBodyPart2);
102+
request5.client.write(requestBodyPart2);
103+
}, headersTimeout * 0.8);
104+
105+
setTimeout(() => {
106+
// After the first timeout, the first request should have been completed and second timedout
107+
assert(request1.completed);
108+
assert(request2.completed);
109+
assert(!request3.completed);
110+
assert(!request4.completed);
111+
assert(!request5.completed);
112+
113+
assert(request1.response.startsWith(responseOk));
114+
assert(request2.response.startsWith(responseTimeout)); // It is expired due to headersTimeout
115+
}, headersTimeout * 1.2 + connectionsCheckingInterval);
116+
117+
setTimeout(() => {
118+
// Complete the body for the fourth request
119+
request4.client.write(requestBodyPart3);
120+
}, headersTimeout * 1.5);
121+
122+
setTimeout(() => {
123+
// All request should be completed now, either with 200 or 408
124+
assert(request3.completed);
125+
assert(request4.completed);
126+
assert(request5.completed);
127+
128+
assert(request3.response.startsWith(responseTimeout)); // It is expired due to requestTimeout
129+
assert(request4.response.startsWith(responseOk));
130+
assert(request5.response.startsWith(responseTimeout)); // It is expired due to requestTimeout
131+
server.close();
132+
}, headersTimeout * 3 + connectionsCheckingInterval);
133+
}));

0 commit comments

Comments
 (0)