Skip to content

Commit 33e62a1

Browse files
committed
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. Update agent doc, add timeout option. Fixes: #21185
1 parent e41ccd4 commit 33e62a1

File tree

6 files changed

+159
-42
lines changed

6 files changed

+159
-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} A number specifying the 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
@@ -647,18 +647,35 @@ function tickOnSocket(req, socket) {
647647
socket.on('end', socketOnEnd);
648648
socket.on('close', socketCloseListener);
649649

650-
if (req.timeout) {
651-
const emitRequestTimeout = () => req.emit('timeout');
652-
socket.once('timeout', emitRequestTimeout);
653-
req.once('response', (res) => {
654-
res.once('end', () => {
655-
socket.removeListener('timeout', emitRequestTimeout);
656-
});
657-
});
650+
if (req.timeout !== undefined) {
651+
listenSocketTimeout(req);
658652
}
659653
req.emit('socket', socket);
660654
}
661655

656+
function listenSocketTimeout(req) {
657+
if (req.timeoutCb) {
658+
return;
659+
}
660+
const emitRequestTimeout = () => req.emit('timeout');
661+
// Set timeoutCb so that it'll get cleaned up on request end
662+
req.timeoutCb = emitRequestTimeout;
663+
// delegate socket timeout event
664+
if (req.socket) {
665+
req.socket.once('timeout', emitRequestTimeout);
666+
} else {
667+
req.on('socket', (socket) => {
668+
socket.once('timeout', emitRequestTimeout);
669+
});
670+
}
671+
// remove socket timeout listener after response end
672+
req.once('response', (res) => {
673+
res.once('end', () => {
674+
req.socket.removeListener('timeout', emitRequestTimeout);
675+
});
676+
});
677+
}
678+
662679
ClientRequest.prototype.onSocket = function onSocket(socket) {
663680
process.nextTick(onSocketNT, this, socket);
664681
};
@@ -708,42 +725,29 @@ function _deferToConnect(method, arguments_, cb) {
708725
}
709726

710727
ClientRequest.prototype.setTimeout = function setTimeout(msecs, callback) {
728+
listenSocketTimeout(this);
711729
msecs = validateTimerDuration(msecs);
712730
if (callback) this.once('timeout', callback);
713731

714-
const emitTimeout = () => this.emit('timeout');
715-
716-
if (this.socket && this.socket.writable) {
717-
if (this.timeoutCb)
718-
this.socket.setTimeout(0, this.timeoutCb);
719-
this.timeoutCb = emitTimeout;
720-
this.socket.setTimeout(msecs, emitTimeout);
721-
return this;
722-
}
723-
724-
// Set timeoutCb so that it'll get cleaned up on request end
725-
this.timeoutCb = emitTimeout;
726732
if (this.socket) {
727-
var sock = this.socket;
728-
this.socket.once('connect', function() {
729-
sock.setTimeout(msecs, emitTimeout);
730-
});
731-
return this;
733+
setSocketTimeout(this.socket, msecs);
734+
} else {
735+
this.once('socket', (sock) => setSocketTimeout(sock, msecs));
732736
}
733737

734-
this.once('socket', function(sock) {
735-
if (sock.connecting) {
736-
sock.once('connect', function() {
737-
sock.setTimeout(msecs, emitTimeout);
738-
});
739-
} else {
740-
sock.setTimeout(msecs, emitTimeout);
741-
}
742-
});
743-
744738
return this;
745739
};
746740

741+
function setSocketTimeout(sock, msecs) {
742+
if (sock.connecting) {
743+
sock.once('connect', function() {
744+
sock.setTimeout(msecs);
745+
});
746+
} else {
747+
sock.setTimeout(msecs);
748+
}
749+
}
750+
747751
ClientRequest.prototype.setNoDelay = function setNoDelay(noDelay) {
748752
this._deferToConnect('setNoDelay', [noDelay]);
749753
};

lib/net.js

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

407407
Socket.prototype.setTimeout = function(msecs, callback) {
408+
this.timeout = msecs;
408409
// Type checking identical to timers.enroll()
409410
msecs = validateTimerDuration(msecs);
410411

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const http = require('http');
5+
6+
const HTTP_CLIENT_TIMEOUT = 2000;
7+
8+
const options = {
9+
method: 'GET',
10+
port: undefined,
11+
host: '127.0.0.1',
12+
path: '/',
13+
timeout: HTTP_CLIENT_TIMEOUT,
14+
};
15+
16+
const server = http.createServer(() => {
17+
// never response
18+
});
19+
20+
server.listen(0, options.host, function() {
21+
doRequest();
22+
});
23+
24+
function doRequest() {
25+
options.port = server.address().port;
26+
const req = http.request(options);
27+
req.setTimeout(HTTP_CLIENT_TIMEOUT / 2);
28+
req.on('error', () => {
29+
// this space is intentionally left blank
30+
});
31+
req.on('close', common.mustCall(() => server.close()));
32+
33+
let timeout_events = 0;
34+
req.on('timeout', common.mustCall(() => {
35+
timeout_events += 1;
36+
}));
37+
req.end();
38+
39+
setTimeout(function() {
40+
req.destroy();
41+
assert.strictEqual(timeout_events, 1);
42+
}, common.platformTimeout(HTTP_CLIENT_TIMEOUT + 50));
43+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const http = require('http');
5+
6+
const HTTP_AGENT_TIMEOUT = 1000;
7+
const HTTP_CLIENT_TIMEOUT = 2000;
8+
9+
const agent = new http.Agent({ timeout: HTTP_AGENT_TIMEOUT });
10+
const options = {
11+
method: 'GET',
12+
port: undefined,
13+
host: '127.0.0.1',
14+
path: '/',
15+
timeout: HTTP_CLIENT_TIMEOUT,
16+
agent,
17+
};
18+
19+
const server = http.createServer(() => {
20+
// never response
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+
const start = Date.now();
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+
const duration = Date.now() - start;
40+
assert((Math.abs(duration - HTTP_CLIENT_TIMEOUT) < 20));
41+
}));
42+
req.end();
43+
44+
setTimeout(function() {
45+
req.destroy();
46+
assert.strictEqual(timeout_events, 1);
47+
}, common.platformTimeout(HTTP_CLIENT_TIMEOUT + 50));
48+
}

0 commit comments

Comments
 (0)