Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to detect connect timeouts #1711

Merged
merged 1 commit into from Aug 10, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
@@ -1,3 +1,4 @@
node_modules
coverage
.idea
npm-debug.log
38 changes: 35 additions & 3 deletions README.md
Expand Up @@ -784,9 +784,12 @@ The first argument can be either a `url` or an `options` object. The only requir
with your pool options or create the pool object with the `maxSockets`
property outside of the loop.
- `timeout` - Integer containing the number of milliseconds to wait for a
request to respond before aborting the request. Note that if the underlying
TCP connection cannot be established, the OS-wide TCP connection timeout will
overrule the `timeout` option ([the default in Linux is around 20 seconds](http://www.sekuda.com/overriding_the_default_linux_kernel_20_second_tcp_socket_connect_timeout)).
server to send response headers (and start the response body) before aborting
the request. Note that if the underlying TCP connection cannot be established,
the OS-wide TCP connection timeout will overrule the `timeout` option ([the
default in Linux can be anywhere from 20-120 seconds][linux-timeout]).

[linux-timeout]: http://www.sekuda.com/overriding_the_default_linux_kernel_20_second_tcp_socket_connect_timeout

---

Expand Down Expand Up @@ -938,6 +941,35 @@ There are at least three ways to debug the operation of `request`:

---

## Timeouts

Most requests to external servers should have a timeout attached, in case the
server is not responding in a timely manner. Without a timeout, your code may
have a socket open/consume resources for minutes or more.

There are two main types of timeouts: **connection timeouts** and **read
timeouts**. A connect timeout occurs if the timeout is hit while your client is
attempting to establish a connection to a remote machine (corresponding to the
[connect() call][connect] on the socket). A read timeout occurs any time the
server is too slow to send back a part of the response.

These two situations have widely different implications for what went wrong
with the request, so it's useful to be able to distinguish them. You can detect
timeout errors by checking `err.code` for an 'ETIMEDOUT' value. Further, you
can detect whether the timeout was a connection timeout by checking if the
`err.connect` property is set to `true`.

```js
request.get('http://10.255.255.1', {timeout: 1500}, function(err) {
console.log(err.code === 'ETIMEDOUT');
// Set to `true` if the timeout was a connection timeout, `false` or
// `undefined` otherwise.
console.log(err.connect === true);
process.exit(0);
});
```

[connect]: http://linux.die.net/man/2/connect

## Examples:

Expand Down
16 changes: 14 additions & 2 deletions request.js
Expand Up @@ -804,21 +804,33 @@ Request.prototype.start = function () {

if (self.timeout && !self.timeoutTimer) {
var timeout = self.timeout < 0 ? 0 : self.timeout
// Set a timeout in memory - this block will throw if the server takes more
// than `timeout` to write the HTTP status and headers (corresponding to
// the on('response') event on the client). NB: this measures wall-clock
// time, not the time between bytes sent by the server.
self.timeoutTimer = setTimeout(function () {
var connectTimeout = self.req.socket && self.req.socket.readable === false
self.abort()
var e = new Error('ETIMEDOUT')
e.code = 'ETIMEDOUT'
e.connect = connectTimeout
self.emit('error', e)
}, timeout)

// Set additional timeout on socket - in case if remote
// server freeze after sending headers
if (self.req.setTimeout) { // only works on node 0.6+
// Set an additional timeout on the socket, via the `setsockopt` syscall.
// This timeout sets the amount of time to wait *between* bytes sent
// from the server, and may or may not correspond to the wall-clock time
// elapsed from the start of the request.
//
// In particular, it's useful for erroring if the server fails to send
// data halfway through streaming a response.
self.req.setTimeout(timeout, function () {
if (self.req) {
self.req.abort()
var e = new Error('ESOCKETTIMEDOUT')
e.code = 'ESOCKETTIMEDOUT'
e.connect = false
self.emit('error', e)
}
})
Expand Down
29 changes: 29 additions & 0 deletions tests/test-timeout.js
Expand Up @@ -43,6 +43,19 @@ if (process.env.TRAVIS === 'true') {
})
})

tape('should set connect to false', function(t) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note Travis skips the tests in this file. I verified the tests passed on my local machine (a Mac). Would appreciate testing in other environments, especially Windows - I'm not sure if Node has the same socket API on Windows machines.

var shouldTimeout = {
url: s.url + '/timeout',
timeout: 100
}

request(shouldTimeout, function(err, res, body) {
checkErrCode(t, err)
t.ok(err.connect === false, 'Read Timeout Error should set \'connect\' property to false')
t.end()
})
})

tape('should timeout with events', function(t) {
t.plan(3)

Expand Down Expand Up @@ -109,6 +122,22 @@ if (process.env.TRAVIS === 'true') {
})
})

tape('connect timeout', function(t) {
// We need a destination that will not immediately return a TCP Reset
// packet. StackOverflow suggests this host:
// https://stackoverflow.com/a/904609/329700
var tarpitHost = 'http://10.255.255.1'
var shouldConnectTimeout = {
url: tarpitHost + '/timeout',
timeout: 100
}
request(shouldConnectTimeout, function(err) {
checkErrCode(t, err)
t.ok(err.connect === true, 'Connect Timeout Error should set \'connect\' property to true')
t.end()
})
})

tape('cleanup', function(t) {
s.close(function() {
t.end()
Expand Down