Skip to content

Commit 949e885

Browse files
killaguaddaleax
authored andcommitted
http: fix request with option timeout and agent
When request with both timeout and agent, timeout not work. This patch will fix it, socket timeout will set to request timeout before socket is connected, and socket timeout will reset to agent timeout after response end. Fixes: #21185 PR-URL: #21204 Reviewed-By: Khaidi Chu <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Ujjwal Sharma <[email protected]>
1 parent d462f8c commit 949e885

File tree

6 files changed

+169
-42
lines changed

6 files changed

+169
-42
lines changed

doc/api/http.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ added: v0.3.4
126126
* `maxFreeSockets` {number} Maximum number of sockets to leave open
127127
in a free state. Only relevant if `keepAlive` is set to `true`.
128128
**Default:** `256`.
129+
* `timeout` {number} Socket timeout in milliseconds.
130+
This will set the timeout after the socket is connected.
129131

130132
The default [`http.globalAgent`][] that is used by [`http.request()`][] has all
131133
of these values set to their respective defaults.

lib/_http_agent.js

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ function Agent(options) {
6666

6767
if (socket.writable &&
6868
this.requests[name] && this.requests[name].length) {
69-
this.requests[name].shift().onSocket(socket);
69+
const req = this.requests[name].shift();
70+
setRequestSocket(this, req, socket);
7071
if (this.requests[name].length === 0) {
7172
// don't leak
7273
delete this.requests[name];
@@ -176,12 +177,12 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
176177
delete this.freeSockets[name];
177178

178179
this.reuseSocket(socket, req);
179-
req.onSocket(socket);
180+
setRequestSocket(this, req, socket);
180181
this.sockets[name].push(socket);
181182
} else if (sockLen < this.maxSockets) {
182183
debug('call onSocket', sockLen, freeLen);
183184
// If we are under maxSockets create a new one.
184-
this.createSocket(req, options, handleSocketCreation(req, true));
185+
this.createSocket(req, options, handleSocketCreation(this, req, true));
185186
} else {
186187
debug('wait for socket');
187188
// We are over limit so we'll add it to the queue.
@@ -305,9 +306,10 @@ Agent.prototype.removeSocket = function removeSocket(s, options) {
305306

306307
if (this.requests[name] && this.requests[name].length) {
307308
debug('removeSocket, have a request, make a socket');
308-
var req = this.requests[name][0];
309+
const req = this.requests[name][0];
309310
// If we have pending requests and a socket gets closed make a new one
310-
this.createSocket(req, options, handleSocketCreation(req, false));
311+
const socketCreationHandler = handleSocketCreation(this, req, false);
312+
this.createSocket(req, options, socketCreationHandler);
311313
}
312314
};
313315

@@ -337,19 +339,36 @@ Agent.prototype.destroy = function destroy() {
337339
}
338340
};
339341

340-
function handleSocketCreation(request, informRequest) {
342+
function handleSocketCreation(agent, request, informRequest) {
341343
return function handleSocketCreation_Inner(err, socket) {
342344
if (err) {
343345
process.nextTick(emitErrorNT, request, err);
344346
return;
345347
}
346348
if (informRequest)
347-
request.onSocket(socket);
349+
setRequestSocket(agent, request, socket);
348350
else
349351
socket.emit('free');
350352
};
351353
}
352354

355+
function setRequestSocket(agent, req, socket) {
356+
req.onSocket(socket);
357+
const agentTimeout = agent.options.timeout || 0;
358+
if (req.timeout === undefined || req.timeout === agentTimeout) {
359+
return;
360+
}
361+
socket.setTimeout(req.timeout);
362+
// reset timeout after response end
363+
req.once('response', (res) => {
364+
res.once('end', () => {
365+
if (socket.timeout !== agentTimeout) {
366+
socket.setTimeout(agentTimeout);
367+
}
368+
});
369+
});
370+
}
371+
353372
function emitErrorNT(emitter, err) {
354373
emitter.emit('error', err);
355374
}

lib/_http_client.js

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -656,18 +656,35 @@ function tickOnSocket(req, socket) {
656656
socket.on('end', socketOnEnd);
657657
socket.on('close', socketCloseListener);
658658

659-
if (req.timeout) {
660-
const emitRequestTimeout = () => req.emit('timeout');
661-
socket.once('timeout', emitRequestTimeout);
662-
req.once('response', (res) => {
663-
res.once('end', () => {
664-
socket.removeListener('timeout', emitRequestTimeout);
665-
});
666-
});
659+
if (req.timeout !== undefined) {
660+
listenSocketTimeout(req);
667661
}
668662
req.emit('socket', socket);
669663
}
670664

665+
function listenSocketTimeout(req) {
666+
if (req.timeoutCb) {
667+
return;
668+
}
669+
const emitRequestTimeout = () => req.emit('timeout');
670+
// Set timeoutCb so it will get cleaned up on request end.
671+
req.timeoutCb = emitRequestTimeout;
672+
// Delegate socket timeout event.
673+
if (req.socket) {
674+
req.socket.once('timeout', emitRequestTimeout);
675+
} else {
676+
req.on('socket', (socket) => {
677+
socket.once('timeout', emitRequestTimeout);
678+
});
679+
}
680+
// Remove socket timeout listener after response end.
681+
req.once('response', (res) => {
682+
res.once('end', () => {
683+
req.socket.removeListener('timeout', emitRequestTimeout);
684+
});
685+
});
686+
}
687+
671688
ClientRequest.prototype.onSocket = function onSocket(socket) {
672689
process.nextTick(onSocketNT, this, socket);
673690
};
@@ -717,42 +734,29 @@ function _deferToConnect(method, arguments_, cb) {
717734
}
718735

719736
ClientRequest.prototype.setTimeout = function setTimeout(msecs, callback) {
737+
listenSocketTimeout(this);
720738
msecs = validateTimerDuration(msecs);
721739
if (callback) this.once('timeout', callback);
722740

723-
const emitTimeout = () => this.emit('timeout');
724-
725-
if (this.socket && this.socket.writable) {
726-
if (this.timeoutCb)
727-
this.socket.setTimeout(0, this.timeoutCb);
728-
this.timeoutCb = emitTimeout;
729-
this.socket.setTimeout(msecs, emitTimeout);
730-
return this;
731-
}
732-
733-
// Set timeoutCb so that it'll get cleaned up on request end
734-
this.timeoutCb = emitTimeout;
735741
if (this.socket) {
736-
var sock = this.socket;
737-
this.socket.once('connect', function() {
738-
sock.setTimeout(msecs, emitTimeout);
739-
});
740-
return this;
742+
setSocketTimeout(this.socket, msecs);
743+
} else {
744+
this.once('socket', (sock) => setSocketTimeout(sock, msecs));
741745
}
742746

743-
this.once('socket', function(sock) {
744-
if (sock.connecting) {
745-
sock.once('connect', function() {
746-
sock.setTimeout(msecs, emitTimeout);
747-
});
748-
} else {
749-
sock.setTimeout(msecs, emitTimeout);
750-
}
751-
});
752-
753747
return this;
754748
};
755749

750+
function setSocketTimeout(sock, msecs) {
751+
if (sock.connecting) {
752+
sock.once('connect', function() {
753+
sock.setTimeout(msecs);
754+
});
755+
} else {
756+
sock.setTimeout(msecs);
757+
}
758+
}
759+
756760
ClientRequest.prototype.setNoDelay = function setNoDelay(noDelay) {
757761
this._deferToConnect('setNoDelay', [noDelay]);
758762
};

lib/net.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ function writeAfterFIN(chunk, encoding, cb) {
408408
}
409409

410410
Socket.prototype.setTimeout = function(msecs, callback) {
411+
this.timeout = msecs;
411412
// Type checking identical to timers.enroll()
412413
msecs = validateTimerDuration(msecs);
413414

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use strict';
2+
const common = require('../common');
3+
4+
// Test that `req.setTimeout` will fired exactly once.
5+
6+
const assert = require('assert');
7+
const http = require('http');
8+
9+
const HTTP_CLIENT_TIMEOUT = 2000;
10+
11+
const options = {
12+
method: 'GET',
13+
port: undefined,
14+
host: '127.0.0.1',
15+
path: '/',
16+
timeout: HTTP_CLIENT_TIMEOUT,
17+
};
18+
19+
const server = http.createServer(() => {
20+
// Never respond.
21+
});
22+
23+
server.listen(0, options.host, function() {
24+
doRequest();
25+
});
26+
27+
function doRequest() {
28+
options.port = server.address().port;
29+
const req = http.request(options);
30+
req.setTimeout(HTTP_CLIENT_TIMEOUT / 2);
31+
req.on('error', () => {
32+
// This space is intentionally left blank.
33+
});
34+
req.on('close', common.mustCall(() => server.close()));
35+
36+
let timeout_events = 0;
37+
req.on('timeout', common.mustCall(() => {
38+
timeout_events += 1;
39+
}));
40+
req.end();
41+
42+
setTimeout(function() {
43+
req.destroy();
44+
assert.strictEqual(timeout_events, 1);
45+
}, common.platformTimeout(HTTP_CLIENT_TIMEOUT));
46+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
const common = require('../common');
3+
4+
// Test that when http request uses both timeout and agent,
5+
// timeout will work as expected.
6+
7+
const assert = require('assert');
8+
const http = require('http');
9+
10+
const HTTP_AGENT_TIMEOUT = 1000;
11+
const HTTP_CLIENT_TIMEOUT = 3000;
12+
13+
const agent = new http.Agent({ timeout: HTTP_AGENT_TIMEOUT });
14+
const options = {
15+
method: 'GET',
16+
port: undefined,
17+
host: '127.0.0.1',
18+
path: '/',
19+
timeout: HTTP_CLIENT_TIMEOUT,
20+
agent,
21+
};
22+
23+
const server = http.createServer(() => {
24+
// Never respond.
25+
});
26+
27+
server.listen(0, options.host, function() {
28+
doRequest();
29+
});
30+
31+
function doRequest() {
32+
options.port = server.address().port;
33+
const req = http.request(options);
34+
const start = Date.now();
35+
req.on('error', () => {
36+
// This space is intentionally left blank.
37+
});
38+
req.on('close', common.mustCall(() => server.close()));
39+
40+
let timeout_events = 0;
41+
req.on('timeout', common.mustCall(() => {
42+
timeout_events += 1;
43+
const duration = Date.now() - start;
44+
// The timeout event cannot be precisely timed. It will delay
45+
// some number of milliseconds, so test it in second units.
46+
assert.strictEqual(duration / 1000 | 0, HTTP_CLIENT_TIMEOUT / 1000);
47+
}));
48+
req.end();
49+
50+
setTimeout(function() {
51+
req.destroy();
52+
assert.strictEqual(timeout_events, 1);
53+
// Ensure the `timeout` event fired only once.
54+
}, common.platformTimeout(HTTP_CLIENT_TIMEOUT * 2));
55+
}

0 commit comments

Comments
 (0)