Skip to content

Commit 2e94eea

Browse files
omsmithronag
authored andcommitted
http: set default timeout in agent keepSocketAlive
Previous location of setting the timeout would override behaviour of custom HttpAgents' keepSocketAlive. Moving it into the default keepSocketAlive allows it to interoperate with custom agents. Fixes: nodejs#33111 PR-URL: nodejs#33127 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: Juan José Arboleda <soyjuanarbol@gmail.com> Reviewed-By: Andrey Pechkurov <apechkurov@gmail.com>
1 parent 8a6fab0 commit 2e94eea

9 files changed

Lines changed: 169 additions & 1 deletion

doc/api/errors.md

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

2277+
<a id="ERR_HTTP_SOCKET_TIMEOUT"></a>
2278+
### ERR_HTTP_SOCKET_TIMEOUT
2279+
2280+
A socket timed out, it's triggered by HTTP.
2281+
22772282
<a id="ERR_INDEX_OUT_OF_RANGE"></a>
22782283
### `ERR_INDEX_OUT_OF_RANGE`
22792284
<!-- YAML

doc/api/http.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,9 @@ changes:
860860
Once a socket is assigned to this request and is connected
861861
[`socket.setTimeout()`][] will be called.
862862

863+
If no `'timeout'` listener is registered the request will error
864+
with `ERR_HTTP_SOCKET_TIMEOUT`.
865+
863866
### `request.socket`
864867
<!-- YAML
865868
added: v0.3.0

lib/_http_agent.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const debug = require('internal/util/debuglog').debuglog('http');
3434
const { async_id_symbol } = require('internal/async_hooks').symbols;
3535
const {
3636
codes: {
37+
ERR_HTTP_SOCKET_TIMEOUT,
3738
ERR_INVALID_ARG_TYPE,
3839
},
3940
} = require('internal/errors');
@@ -339,8 +340,13 @@ function installListeners(agent, s, options) {
339340
function onTimeout() {
340341
debug('CLIENT socket onTimeout');
341342

343+
if (s.listenerCount('timeout') === 1) {
344+
// No req timeout handler, agent must destroy the socket.
345+
s.destroy(new ERR_HTTP_SOCKET_TIMEOUT());
346+
return;
347+
}
348+
342349
// Destroy if in free list.
343-
// TODO(ronag): Always destroy, even if not in free list.
344350
const sockets = agent.freeSockets;
345351
for (const name of ObjectKeys(sockets)) {
346352
if (sockets[name].includes(s)) {

lib/internal/errors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,7 @@ E('ERR_HTTP_HEADERS_SENT',
915915
E('ERR_HTTP_INVALID_HEADER_VALUE',
916916
'Invalid value "%s" for header "%s"', TypeError);
917917
E('ERR_HTTP_INVALID_STATUS_CODE', 'Invalid status code: %s', RangeError);
918+
E('ERR_HTTP_SOCKET_TIMEOUT', 'Socket timeout', Error);
918919
E('ERR_HTTP_TRAILER_INVALID',
919920
'Trailers are invalid with this transfer encoding', Error);
920921
E('ERR_INCOMPATIBLE_OPTION_PAIR',

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-agent-timeout.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,43 @@ const http = require('http');
132132
}));
133133
}));
134134
}
135+
136+
{
137+
// Ensure custom keepSocketAlive timeout is respected
138+
139+
const CUSTOM_TIMEOUT = 60;
140+
const AGENT_TIMEOUT = 50;
141+
142+
class CustomAgent extends http.Agent {
143+
keepSocketAlive(socket) {
144+
if (!super.keepSocketAlive(socket)) {
145+
return false;
146+
}
147+
148+
socket.setTimeout(CUSTOM_TIMEOUT);
149+
return true;
150+
}
151+
}
152+
153+
const agent = new CustomAgent({ keepAlive: true, timeout: AGENT_TIMEOUT });
154+
155+
const server = http.createServer((req, res) => {
156+
res.end();
157+
});
158+
159+
server.listen(0, common.mustCall(() => {
160+
http.get({ port: server.address().port, agent })
161+
.on('response', common.mustCall((res) => {
162+
const socket = res.socket;
163+
assert(socket);
164+
res.resume();
165+
socket.on('free', common.mustCall(() => {
166+
socket.on('timeout', common.mustCall(() => {
167+
assert.strictEqual(socket.timeout, CUSTOM_TIMEOUT);
168+
agent.destroy();
169+
server.close();
170+
}));
171+
}));
172+
}));
173+
}));
174+
}
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)