Skip to content

Commit bba500d

Browse files
killagutargos
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 3d93273 commit bba500d

6 files changed

+169
-42
lines changed

doc/api/http.md

+2
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

+26-7
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

+39-35
Original file line numberDiff line numberDiff line change
@@ -639,18 +639,35 @@ function tickOnSocket(req, socket) {
639639
socket.on('end', socketOnEnd);
640640
socket.on('close', socketCloseListener);
641641

642-
if (req.timeout) {
643-
const emitRequestTimeout = () => req.emit('timeout');
644-
socket.once('timeout', emitRequestTimeout);
645-
req.once('response', (res) => {
646-
res.once('end', () => {
647-
socket.removeListener('timeout', emitRequestTimeout);
648-
});
649-
});
642+
if (req.timeout !== undefined) {
643+
listenSocketTimeout(req);
650644
}
651645
req.emit('socket', socket);
652646
}
653647

648+
function listenSocketTimeout(req) {
649+
if (req.timeoutCb) {
650+
return;
651+
}
652+
const emitRequestTimeout = () => req.emit('timeout');
653+
// Set timeoutCb so it will get cleaned up on request end.
654+
req.timeoutCb = emitRequestTimeout;
655+
// Delegate socket timeout event.
656+
if (req.socket) {
657+
req.socket.once('timeout', emitRequestTimeout);
658+
} else {
659+
req.on('socket', (socket) => {
660+
socket.once('timeout', emitRequestTimeout);
661+
});
662+
}
663+
// Remove socket timeout listener after response end.
664+
req.once('response', (res) => {
665+
res.once('end', () => {
666+
req.socket.removeListener('timeout', emitRequestTimeout);
667+
});
668+
});
669+
}
670+
654671
ClientRequest.prototype.onSocket = function onSocket(socket) {
655672
process.nextTick(onSocketNT, this, socket);
656673
};
@@ -700,42 +717,29 @@ function _deferToConnect(method, arguments_, cb) {
700717
}
701718

702719
ClientRequest.prototype.setTimeout = function setTimeout(msecs, callback) {
720+
listenSocketTimeout(this);
703721
msecs = validateTimerDuration(msecs);
704722
if (callback) this.once('timeout', callback);
705723

706-
const emitTimeout = () => this.emit('timeout');
707-
708-
if (this.socket && this.socket.writable) {
709-
if (this.timeoutCb)
710-
this.socket.setTimeout(0, this.timeoutCb);
711-
this.timeoutCb = emitTimeout;
712-
this.socket.setTimeout(msecs, emitTimeout);
713-
return this;
714-
}
715-
716-
// Set timeoutCb so that it'll get cleaned up on request end
717-
this.timeoutCb = emitTimeout;
718724
if (this.socket) {
719-
var sock = this.socket;
720-
this.socket.once('connect', function() {
721-
sock.setTimeout(msecs, emitTimeout);
722-
});
723-
return this;
725+
setSocketTimeout(this.socket, msecs);
726+
} else {
727+
this.once('socket', (sock) => setSocketTimeout(sock, msecs));
724728
}
725729

726-
this.once('socket', function(sock) {
727-
if (sock.connecting) {
728-
sock.once('connect', function() {
729-
sock.setTimeout(msecs, emitTimeout);
730-
});
731-
} else {
732-
sock.setTimeout(msecs, emitTimeout);
733-
}
734-
});
735-
736730
return this;
737731
};
738732

733+
function setSocketTimeout(sock, msecs) {
734+
if (sock.connecting) {
735+
sock.once('connect', function() {
736+
sock.setTimeout(msecs);
737+
});
738+
} else {
739+
sock.setTimeout(msecs);
740+
}
741+
}
742+
739743
ClientRequest.prototype.setNoDelay = function setNoDelay(noDelay) {
740744
this._deferToConnect('setNoDelay', [noDelay]);
741745
};

lib/net.js

+1
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

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+
}
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)