Skip to content

Commit be95679

Browse files
committed
http: destroy timeout socket by Agent
Agent must destroy timeout socket when there is no any timeout handler. Avoid socket hang on forever when the server don't send any response back.
1 parent 2cb2597 commit be95679

File tree

9 files changed

+148
-2
lines changed

9 files changed

+148
-2
lines changed

doc/api/errors.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,6 +1912,11 @@ removed: v10.0.0
19121912
Used when an invalid character is found in an HTTP response status message
19131913
(reason phrase).
19141914

1915+
<a id="ERR_HTTP_SOCKET_TIMEOUT"></a>
1916+
### ERR_HTTP_SOCKET_TIMEOUT
1917+
1918+
A socket timed out, it's triggered by HTTP.
1919+
19151920
<a id="ERR_INDEX_OUT_OF_RANGE"></a>
19161921
### ERR_INDEX_OUT_OF_RANGE
19171922
<!-- YAML

doc/api/http.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,9 @@ added: v0.5.9
692692
Once a socket is assigned to this request and is connected
693693
[`socket.setTimeout()`][] will be called.
694694

695+
Emitted when the underlying socket times out from inactivity.
696+
This only notifies that the socket has been idle. The request must be aborted manually.
697+
695698
### request.socket
696699
<!-- YAML
697700
added: v0.3.0

lib/_http_agent.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const util = require('util');
2626
const EventEmitter = require('events');
2727
const debug = util.debuglog('http');
2828
const { async_id_symbol } = require('internal/async_hooks').symbols;
29+
const errors = require('internal/errors');
30+
const {
31+
ERR_HTTP_SOCKET_TIMEOUT,
32+
} = errors.codes;
2933

3034
// New Agent code.
3135

@@ -268,6 +272,24 @@ function installListeners(agent, s, options) {
268272
}
269273
s.on('close', onClose);
270274

275+
function onTimeout() {
276+
debug('CLIENT socket onTimeout after', s.timeout, 'ms');
277+
const name = agent.getName(options);
278+
// If free socket timeout, must remove it from freeSockets list immediately
279+
// to prevent new requests from being sent through this timeout socket.
280+
if (agent.freeSockets[name] && agent.freeSockets[name].indexOf(s) !== -1) {
281+
s.destroy();
282+
agent.removeSocket(s, options);
283+
debug('CLIENT free socket destroy');
284+
} else if (s.listeners('timeout').length === 1) {
285+
// No req timeout handler, agent must destroy the socket.
286+
s.destroy(new ERR_HTTP_SOCKET_TIMEOUT());
287+
agent.removeSocket(s, options);
288+
debug('CLIENT active socket destroy');
289+
}
290+
}
291+
s.on('timeout', onTimeout);
292+
271293
function onRemove() {
272294
// We need this function for cases like HTTP 'upgrade'
273295
// (defined by WebSockets) where we need to remove a socket from the
@@ -276,6 +298,7 @@ function installListeners(agent, s, options) {
276298
agent.removeSocket(s, options);
277299
s.removeListener('close', onClose);
278300
s.removeListener('free', onFree);
301+
s.removeListener('timeout', onTimeout);
279302
s.removeListener('agentRemove', onRemove);
280303
}
281304
s.on('agentRemove', onRemove);

lib/internal/errors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,7 @@ E('ERR_HTTP_HEADERS_SENT',
664664
E('ERR_HTTP_INVALID_HEADER_VALUE',
665665
'Invalid value "%s" for header "%s"', TypeError);
666666
E('ERR_HTTP_INVALID_STATUS_CODE', 'Invalid status code: %s', RangeError);
667+
E('ERR_HTTP_SOCKET_TIMEOUT', 'Socket timeout', Error);
667668
E('ERR_HTTP_TRAILER_INVALID',
668669
'Trailers are invalid with this transfer encoding', Error);
669670
E('ERR_INSPECTOR_ALREADY_CONNECTED', '%s is already connected', Error);

test/parallel/test-gc-http-client-timeout.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
const common = require('../common');
77
const onGC = require('../common/ongc');
8+
const assert = require('assert');
89

910
function serverHandler(req, res) {
1011
setTimeout(function() {
@@ -38,6 +39,11 @@ function getall() {
3839
req.setTimeout(10, function() {
3940
console.log('timeout (expected)');
4041
});
42+
req.on('error', (err) => {
43+
// only allow Socket timeout error
44+
assert.strictEqual(err.code, 'ERR_SOCKET_TIMEOUT');
45+
assert.strictEqual(err.message, 'Socket timeout');
46+
});
4147

4248
count++;
4349
onGC(req, { ongc });

test/parallel/test-http-client-timeout-option-listeners.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ const options = {
2424
server.listen(0, options.host, common.mustCall(() => {
2525
options.port = server.address().port;
2626
doRequest(common.mustCall((numListeners) => {
27-
assert.strictEqual(numListeners, 1);
27+
// including the default onTimeout on Agent
28+
assert.strictEqual(numListeners, 2);
2829
doRequest(common.mustCall((numListeners) => {
29-
assert.strictEqual(numListeners, 1);
30+
assert.strictEqual(numListeners, 2);
3031
server.close();
3132
agent.destroy();
3233
}));
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const http = require('http');
5+
6+
const server = http.createServer(common.mustCall((req, res) => {
7+
server.close();
8+
// do nothing, wait client socket timeout and close
9+
}));
10+
11+
server.listen(0, common.mustCall(() => {
12+
const agent = new http.Agent({ timeout: 100 });
13+
const req = http.get({
14+
path: '/',
15+
port: server.address().port,
16+
timeout: 50,
17+
agent
18+
}, common.mustNotCall())
19+
.on('error', common.mustCall((err) => {
20+
assert.strictEqual(err.message, 'socket hang up');
21+
assert.strictEqual(err.code, 'ECONNRESET');
22+
}));
23+
req.on('timeout', common.mustCall(() => {
24+
req.abort();
25+
}));
26+
}));
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const http = require('http');
5+
6+
const server = http.createServer(common.mustCall((req, res) => {
7+
server.close();
8+
// do nothing, wait client socket timeout and close
9+
}));
10+
11+
server.listen(0, common.mustCall(() => {
12+
let req;
13+
const timer = setTimeout(() => {
14+
req.abort();
15+
assert.fail('should not timeout here');
16+
}, 100);
17+
18+
const agent = new http.Agent({ timeout: 50 });
19+
req = http.get({
20+
path: '/',
21+
port: server.address().port,
22+
agent
23+
}, common.mustNotCall())
24+
.on('error', common.mustCall((err) => {
25+
clearTimeout(timer);
26+
assert.strictEqual(err.message, 'Socket timeout');
27+
assert.strictEqual(err.code, 'ERR_HTTP_SOCKET_TIMEOUT');
28+
}));
29+
}));
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const http = require('http');
5+
6+
const server = http.createServer(common.mustCallAtLeast((req, res) => {
7+
const content = 'Hello Agent';
8+
res.writeHead(200, {
9+
'Content-Length': content.length.toString(),
10+
});
11+
res.write(content);
12+
res.end();
13+
}, 2));
14+
15+
server.listen(0, common.mustCall(() => {
16+
const agent = new http.Agent({ timeout: 100, keepAlive: true });
17+
const req = http.get({
18+
path: '/',
19+
port: server.address().port,
20+
agent
21+
}, common.mustCall((res) => {
22+
assert.strictEqual(res.statusCode, 200);
23+
res.resume();
24+
res.on('end', common.mustCall());
25+
}));
26+
27+
const timer = setTimeout(() => {
28+
assert.fail('should not timeout here');
29+
req.abort();
30+
}, 1000);
31+
32+
req.on('socket', common.mustCall((socket) => {
33+
// wait free socket become free and timeout
34+
socket.on('timeout', common.mustCall(() => {
35+
// free socket should be destroyed
36+
assert.strictEqual(socket.writable, false);
37+
// send new request will be fails
38+
clearTimeout(timer);
39+
const newReq = http.get({
40+
path: '/',
41+
port: server.address().port,
42+
agent
43+
}, common.mustCall((res) => {
44+
// agent must create a new socket to handle request
45+
assert.notStrictEqual(newReq.socket, socket);
46+
assert.strictEqual(res.statusCode, 200);
47+
res.resume();
48+
res.on('end', common.mustCall(() => server.close()));
49+
}));
50+
}));
51+
}));
52+
}));

0 commit comments

Comments
 (0)