Skip to content

Commit a47b782

Browse files
committed
fix(retry): be more specific about when we retry
Retries are now attempted only for specific known errors. All other will fail fast. Additionally, requests will be retried on 404s and rate-limiting status codes 420 (for Twitter) and 429, along with the original 408 and 500-level error codes. All other codes will continue to fast-path to request failure. One big effect of this is that requests are no longer retried on ENOTFOUND -- that is, the getaddrinfo DNS failure error. When this happens, the problem is almost certainly that the user is currently offline. In combination with conditional requests and the cache rules, this means that make-fetch-happen now has effectively an "offline mode", where the `default` cache mode will only fail on ENOTFOUND if there is no cache entry available. As before, if we're using a stale locally-cached request due to an error (again, such as ENOTFOUND), a `Warning: 111` header will be added to the response, so you can check that field to figure out what make-fetch-happen did. BREAKING CHANGE: Retry logic has changes. * 404s, 420s, and 429s all retry now. * ENOTFOUND no longer retries. * Only ECONNRESET, ECONNREFUSED, EADDRINUSE, ETIMEDOUT, and `request-timeout` errors are retried.
1 parent 66e5e87 commit a47b782

File tree

1 file changed

+36
-5
lines changed

1 file changed

+36
-5
lines changed

index.js

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@ const retry = require('promise-retry')
88
let ssri
99
const Stream = require('stream')
1010

11+
const RETRY_ERRORS = [
12+
'ECONNRESET', // remote socket closed on us
13+
'ECONNREFUSED', // remote host refused to open connection
14+
'EADDRINUSE', // failed to bind to a local port (proxy?)
15+
'ETIMEDOUT' // someone in the transaction is WAY TOO SLOW
16+
// Known codes we do NOT retry on:
17+
// ENOTFOUND (getaddrinfo failure. Either bad hostname, or offline)
18+
]
19+
20+
const RETRY_TYPES = [
21+
'request-timeout'
22+
]
23+
1124
// https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
1225
module.exports = cachingFetch
1326
cachingFetch.defaults = function (_uri, _opts) {
@@ -196,7 +209,7 @@ function condFetch (uri, cachedRes, opts) {
196209
if (ctrl.match(/must-revalidate/i)) {
197210
throw err
198211
} else {
199-
setWarning(cachedRes, 111, `Unexpected error: ${err.message}`)
212+
setWarning(cachedRes, 111, `${err.code}: ${err.message}`)
200213
return cachedRes
201214
}
202215
})
@@ -280,8 +293,18 @@ function remoteFetch (uri, opts) {
280293
return res
281294
}
282295
})
283-
} else if (res.status === 408 || res.status >= 500) {
284-
// 408 === timeout
296+
} else if (
297+
// Retriable + rate-limiting status codes
298+
// When hitting an API with rate-limiting features,
299+
// be sure to set the `retry` settings according to
300+
// documentation for that.
301+
res.status === 404 || // Not Found ("subsequent requests permissible")
302+
res.status === 408 || // Request Timeout
303+
res.status === 420 || // Enhance Your Calm (usually Twitter rate-limit)
304+
res.status === 429 || // Too Many Requests ("standard" rate-limiting)
305+
// Assume server errors are momentary hiccups
306+
res.status >= 500
307+
) {
285308
if (req.method === 'POST') {
286309
return res
287310
} else if (req.body instanceof Stream) {
@@ -293,14 +316,22 @@ function remoteFetch (uri, opts) {
293316
return res
294317
}
295318
}).catch(err => {
296-
if (req.method !== 'POST') {
319+
const code = err.code === 'EPROMISERETRY'
320+
? err.retried.code
321+
: err.code
322+
if (
323+
req.method !== 'POST' && (
324+
RETRY_ERRORS.indexOf(code) >= 0 ||
325+
RETRY_TYPES.indexOf(err.type) >= 0
326+
)
327+
) {
297328
return retryHandler(err)
298329
} else {
299330
throw err
300331
}
301332
})
302333
}, opts.retry === false ? { retries: 0 } : opts.retry).catch(err => {
303-
if (err.status >= 500 || err.status === 408) {
334+
if (err.status >= 400) {
304335
return err
305336
} else {
306337
throw err

0 commit comments

Comments
 (0)