Skip to content

Commit

Permalink
Add retry.afterStatusCodes option (#598)
Browse files Browse the repository at this point in the history
Co-authored-by: Seth Holladay <me@seth-holladay.com>
  • Loading branch information
Kenneth-Sills and sholladay authored Jul 25, 2024
1 parent 4298183 commit 8764419
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 12 deletions.
5 changes: 4 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,17 @@ Default:
- `limit`: `2`
- `methods`: `get` `put` `head` `delete` `options` `trace`
- `statusCodes`: [`408`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) [`413`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413) [`429`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) [`500`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) [`502`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) [`503`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) [`504`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504)
- `afterStatusCodes`: [`413`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413), [`429`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429), [`503`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503)
- `maxRetryAfter`: `undefined`
- `backoffLimit`: `undefined`
- `delay`: `attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000`

An object representing `limit`, `methods`, `statusCodes` and `maxRetryAfter` fields for maximum retry count, allowed methods, allowed status codes and maximum [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) time.
An object representing `limit`, `methods`, `statusCodes`, `afterStatusCodes`, and `maxRetryAfter` fields for maximum retry count, allowed methods, allowed status codes, status codes allowed to use the [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) time, and maximum [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) time.

If `retry` is a number, it will be used as `limit` and other defaults will remain in place.

If the response provides an HTTP status contained in `afterStatusCodes`, Ky will wait until the date or timeout given in the [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header has passed to retry the request. If the provided status code is not in the list, the [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header will be ignored.

If `maxRetryAfter` is set to `undefined`, it will use `options.timeout`. If [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header is greater than `maxRetryAfter`, it will use `maxRetryAfter`.

The `backoffLimit` option is the upper limit of the delay per retry in milliseconds.
Expand Down
4 changes: 3 additions & 1 deletion source/types/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,12 @@ export type KyOptions = {
prefixUrl?: URL | string;

/**
An object representing `limit`, `methods`, `statusCodes` and `maxRetryAfter` fields for maximum retry count, allowed methods, allowed status codes and maximum [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) time.
An object representing `limit`, `methods`, `statusCodes`, `afterStatusCodes`, and `maxRetryAfter` fields for maximum retry count, allowed methods, allowed status codes, status codes allowed to use the [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) time, and maximum [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) time.
If `retry` is a number, it will be used as `limit` and other defaults will remain in place.
If the response provides an HTTP status contained in `afterStatusCodes`, Ky will wait until the date or timeout given in the [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header has passed to retry the request. If the provided status code is not in the list, the [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header will be ignored.
If `maxRetryAfter` is set to `undefined`, it will use `options.timeout`. If [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header is greater than `maxRetryAfter`, it will cancel the request.
By default, delays between retries are calculated with the function `0.3 * (2 ** (attemptCount - 1)) * 1000`, where `attemptCount` is the attempt number (starts from 1), however this can be changed by passing a `delay` function.
Expand Down
1 change: 0 additions & 1 deletion source/utils/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,5 @@ export const normalizeRetryOptions = (retry: number | RetryOptions = {}): Requir
return {
...defaultRetryOptions,
...retry,
afterStatusCodes: retryAfterStatusCodes,
};
};
50 changes: 41 additions & 9 deletions test/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {withPerformance} from './helpers/with-performance.js';

const fixture = 'fixture';
const defaultRetryCount = 2;
const retryAfterOn500 = 2;
const retryAfterOn413 = 2;
const lastTried413access = Date.now();

test('network error', async t => {
let requestCount = 0;
Expand All @@ -23,6 +23,7 @@ test('network error', async t => {
});

t.is(await ky(server.url).text(), fixture);
t.is(requestCount, defaultRetryCount + 1);

await server.close();
});
Expand All @@ -42,6 +43,7 @@ test('status code 500', async t => {
});

t.is(await ky(server.url).text(), fixture);
t.is(requestCount, defaultRetryCount + 1);

await server.close();
});
Expand All @@ -61,6 +63,7 @@ test('only on defined status codes', async t => {
});

await t.throwsAsync(ky(server.url).text(), {message: /Bad Request/});
t.is(requestCount, 1);

await server.close();
});
Expand All @@ -82,6 +85,7 @@ test('not on POST', async t => {
await t.throwsAsync(ky.post(server.url).text(), {
message: /Internal Server Error/,
});
t.is(requestCount, 1);

await server.close();
});
Expand Down Expand Up @@ -121,14 +125,15 @@ test('respect Retry-After: 0 and retry immediately', async t => {
});

test('respect 413 Retry-After', async t => {
const startTime = Date.now();
let requestCount = 0;

const server = await createHttpTestServer();
server.get('/', (_request, response) => {
requestCount++;

if (requestCount === defaultRetryCount + 1) {
response.end((Date.now() - lastTried413access).toString());
response.end((Date.now() - startTime).toString());
} else {
response.writeHead(413, {
'Retry-After': retryAfterOn413,
Expand All @@ -137,20 +142,22 @@ test('respect 413 Retry-After', async t => {
}
});

const result = await ky(server.url).text();
t.true(Number(result) >= retryAfterOn413 * 1000);
const timeElapsedInMs = Number(await ky(server.url).text());
t.true(timeElapsedInMs >= retryAfterOn413 * 1000);
t.is(requestCount, retryAfterOn413 + 1);

await server.close();
});

test('respect 413 Retry-After with timestamp', async t => {
const startTime = Date.now();
let requestCount = 0;

const server = await createHttpTestServer({bodyParser: false});
server.get('/', (_request, response) => {
requestCount++;
if (requestCount === defaultRetryCount + 1) {
response.end((Date.now() - lastTried413access).toString());
response.end((Date.now() - startTime).toString());
} else {
// @NOTE we need to round up to the next second due to http-date resolution
const date = new Date(Date.now() + ((retryAfterOn413 + 1) * 1000)).toUTCString();
Expand All @@ -161,9 +168,9 @@ test('respect 413 Retry-After with timestamp', async t => {
}
});

const result = await ky(server.url).text();
t.true(Number(result) >= retryAfterOn413 * 1000);
t.is(requestCount, 3);
const timeElapsedInMs = Number(await ky(server.url).text());
t.true(timeElapsedInMs >= retryAfterOn413 * 1000);
t.is(requestCount, retryAfterOn413 + 1);

await server.close();
});
Expand All @@ -185,6 +192,31 @@ test('doesn\'t retry on 413 without Retry-After header', async t => {
await server.close();
});

test('respect custom `afterStatusCodes` (500) with Retry-After header', async t => {
const startTime = Date.now();
let requestCount = 0;

const server = await createHttpTestServer();
server.get('/', (_request, response) => {
requestCount++;

if (requestCount === defaultRetryCount + 1) {
response.end((Date.now() - startTime).toString());
} else {
response.writeHead(500, {
'Retry-After': retryAfterOn500,
});
response.end('');
}
});

const timeElapsedInMs = Number(await ky(server.url, {retry: {afterStatusCodes: [500]}}).text());
t.true(timeElapsedInMs >= retryAfterOn500 * 1000);
t.is(requestCount, retryAfterOn500 + 1);

await server.close();
});

test('respect number of retries', async t => {
let requestCount = 0;

Expand Down Expand Up @@ -249,7 +281,7 @@ test('respect retry methods', async t => {
message: /Request Timeout/,
},
);
t.is(requestCount, 3);
t.is(requestCount, defaultRetryCount + 1);

await server.close();
});
Expand Down

0 comments on commit 8764419

Please sign in to comment.