Skip to content
This repository has been archived by the owner on Feb 12, 2022. It is now read-only.

Commit

Permalink
Retry on 429 and respect the Retry-After header (#31)
Browse files Browse the repository at this point in the history
* fix retries option is not fully respected, fix not to throw error caused by error status

* retry when http status is 429 as well, respect the Retry-After header

* fix readme

* revert default retries

* add maxRetryAfter header
  • Loading branch information
nkzawa authored Mar 3, 2020
1 parent 1e065de commit 559a81b
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 3 deletions.
13 changes: 12 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const debug = require('debug')('fetch-retry');
// retry settings
const MIN_TIMEOUT = 10;
const MAX_RETRIES = 5;
const MAX_RETRY_AFTER = 20;
const FACTOR = 6;

module.exports = exports = setup;
Expand All @@ -20,6 +21,7 @@ function setup(fetch) {
minTimeout: MIN_TIMEOUT,
retries: MAX_RETRIES,
factor: FACTOR,
maxRetryAfter: MAX_RETRY_AFTER,
}, opts.retry);

if (opts.onRetry) {
Expand All @@ -38,7 +40,16 @@ function setup(fetch) {
// this will be retried
const res = await fetch(url, opts);
debug('status %d', res.status);
if (res.status >= 500 && res.status < 600) {
if ((res.status >= 500 && res.status < 600) || res.status === 429) {
// NOTE: doesn't support http-date format
const retryAfter = parseInt(res.headers.get('retry-after'), 10);
if (retryAfter) {
if (retryAfter > retryOpts.maxRetryAfter) {
return res;
} else {
await new Promise(r => setTimeout(r, retryAfter * 1e3));
}
}
throw new ResponseError(res);
} else {
return res;
Expand Down
9 changes: 7 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ module.exports = async () => {
Make sure to `yarn add @zeit/fetch-retry` in your main package.

Note that you can pass [retry options](https://github.com/zeit/async-retry) to using `opts.retry`.
We also provide a `opts.onRetry` which is a customized version of `opts.retry.onRetry` and passes
We also provide a `opts.onRetry` and `opts.retry.maxRetryAfter` options.

`opts.onRetry` is a customized version of `opts.retry.onRetry` and passes
not only the `error` object in each retry but also the current `opts` object.

`opts.retry.maxRetryAfter` is the max wait time according to the `Retry-After` header.
If it exceeds the option value, stop retrying and returns the error response. It defaults to `20`.

## Rationale

Some errors are very common in production (like the underlying `Socket`
Expand All @@ -30,7 +35,7 @@ by retrying.

The default behavior of `fetch-retry` is to attempt retries **10**, **60**
**360**, **2160** and **12960** milliseconds (a total of 5 retries) after
a *network error* or *5xx* error occur.
a *network error*, *429* or *5xx* error occur.

The idea is to provide a sensible default: most applications should
continue to perform correctly with a worst case scenario of a given
Expand Down
58 changes: 58 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,61 @@ test('accepts a custom onRetry option', async () => {
server.on('error', reject);
});
})

test('handles the Retry-After header', async () => {
const server = createServer((req, res) => {
res.writeHead(429, { 'Retry-After': 1 });
res.end();
});

return new Promise((resolve, reject) => {
server.listen(async () => {
const {port} = server.address();
try {
const startedAt = Date.now();
const res = await retryFetch(`http://127.0.0.1:${port}`, {
retry: {
minTimeout: 10,
retries: 1
}
});
expect(Date.now() - startedAt).toBeGreaterThanOrEqual(1010);
resolve();
} catch (err) {
reject(err);
} finally {
server.close();
}
});
server.on('error', reject);
});
});

test('stops retrying when the Retry-After header exceeds the maxRetryAfter option', async () => {
const server = createServer((req, res) => {
res.writeHead(429, { 'Retry-After': 21 });
res.end();
});

return new Promise((resolve, reject) => {
const opts = {
onRetry: jest.fn(),
}

server.listen(async () => {
const {port} = server.address();
try {
const startedAt = Date.now();
const res = await retryFetch(`http://127.0.0.1:${port}`, opts);
expect(opts.onRetry.mock.calls.length).toBe(0);
expect(res.status).toBe(429);
resolve();
} catch (err) {
reject(err);
} finally {
server.close();
}
});
server.on('error', reject);
});
});

0 comments on commit 559a81b

Please sign in to comment.