Skip to content

Commit

Permalink
Add support for the RateLimit-Reset header (#618)
Browse files Browse the repository at this point in the history
  • Loading branch information
sholladay committed Aug 12, 2024
1 parent fbe0ec6 commit 8531b05
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 3 deletions.
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ An object representing `limit`, `methods`, `statusCodes`, `afterStatusCodes`, an

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 the response provides an HTTP status contained in `afterStatusCodes`, Ky will wait until the date, timeout, or timestamp 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 `Retry-After` is missing, the non-standard [`RateLimit-Reset`](https://www.ietf.org/archive/id/draft-polli-ratelimit-headers-05.html#section-3.3) header is used in its place as a fallback. 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`.

Expand Down
8 changes: 7 additions & 1 deletion source/core/Ky.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,17 @@ export class Ky {
throw error;
}

const retryAfter = error.response.headers.get('Retry-After');
const retryAfter = error.response.headers.get('Retry-After')
?? error.response.headers.get('RateLimit-Reset')
?? error.response.headers.get('X-RateLimit-Reset') // GitHub
?? error.response.headers.get('X-Rate-Limit-Reset'); // Twitter
if (retryAfter && this._options.retry.afterStatusCodes.includes(error.response.status)) {
let after = Number(retryAfter) * 1000;
if (Number.isNaN(after)) {
after = Date.parse(retryAfter) - Date.now();
} else if (after >= Date.parse('2024-01-01')) {
// A large number is treated as a timestamp (fixed threshold protects against clock skew)
after -= Date.now();
}

const max = this._options.retry.maxRetryAfter ?? after;
Expand Down
2 changes: 1 addition & 1 deletion source/types/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export type KyOptions = {
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 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 `Retry-After` is missing, the non-standard [`RateLimit-Reset`](https://www.ietf.org/archive/id/draft-polli-ratelimit-headers-02.html#section-3.3) header is used in its place as a fallback. 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.
Expand Down
65 changes: 65 additions & 0 deletions test/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,71 @@ test('respect Retry-After: 0 and retry immediately', async t => {
await server.close();
});

test('RateLimit-Reset is treated the same as Retry-After', async t => {
let requestCount = 0;

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

if (requestCount === defaultRetryCount + 1) {
response.end(fixture);
} else {
const header = (requestCount < 2) ? 'RateLimit-Reset' : 'Retry-After';
response.writeHead(429, {
[header]: 1,
});

response.end('');
}
});

await withPerformance({
t,
expectedDuration: 1000 + 1000,
async test() {
t.is(await ky(server.url).text(), fixture);
},
});

t.is(requestCount, 3);

await server.close();
});

test('RateLimit-Reset with time since epoch', async t => {
let requestCount = 0;

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

if (requestCount === defaultRetryCount + 1) {
response.end(fixture);
} else {
const twoSecondsByDelta = 2;
const oneSecondByEpoch = (Date.now() / 1000) + 1;
response.writeHead(429, {
'RateLimit-Reset': (requestCount < 2) ? twoSecondsByDelta : oneSecondByEpoch,
});

response.end('');
}
});

await withPerformance({
t,
expectedDuration: 2000 + 1000,
async test() {
t.is(await ky(server.url).text(), fixture);
},
});

t.is(requestCount, 3);

await server.close();
});

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

0 comments on commit 8531b05

Please sign in to comment.