Skip to content

Commit 8531b05

Browse files
authoredAug 12, 2024··
Add support for the RateLimit-Reset header (#618)
1 parent fbe0ec6 commit 8531b05

File tree

4 files changed

+74
-3
lines changed

4 files changed

+74
-3
lines changed
 

‎readme.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ An object representing `limit`, `methods`, `statusCodes`, `afterStatusCodes`, an
208208

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

211-
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.
211+
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.
212212

213213
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`.
214214

‎source/core/Ky.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,17 @@ export class Ky {
217217
throw error;
218218
}
219219

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

227233
const max = this._options.retry.maxRetryAfter ?? after;

‎source/types/options.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export type KyOptions = {
120120
121121
If `retry` is a number, it will be used as `limit` and other defaults will remain in place.
122122
123-
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.
123+
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.
124124
125125
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.
126126

‎test/retry.ts

+65
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,71 @@ test('respect Retry-After: 0 and retry immediately', async t => {
124124
await server.close();
125125
});
126126

127+
test('RateLimit-Reset is treated the same as Retry-After', async t => {
128+
let requestCount = 0;
129+
130+
const server = await createHttpTestServer();
131+
server.get('/', (_request, response) => {
132+
requestCount++;
133+
134+
if (requestCount === defaultRetryCount + 1) {
135+
response.end(fixture);
136+
} else {
137+
const header = (requestCount < 2) ? 'RateLimit-Reset' : 'Retry-After';
138+
response.writeHead(429, {
139+
[header]: 1,
140+
});
141+
142+
response.end('');
143+
}
144+
});
145+
146+
await withPerformance({
147+
t,
148+
expectedDuration: 1000 + 1000,
149+
async test() {
150+
t.is(await ky(server.url).text(), fixture);
151+
},
152+
});
153+
154+
t.is(requestCount, 3);
155+
156+
await server.close();
157+
});
158+
159+
test('RateLimit-Reset with time since epoch', async t => {
160+
let requestCount = 0;
161+
162+
const server = await createHttpTestServer();
163+
server.get('/', (_request, response) => {
164+
requestCount++;
165+
166+
if (requestCount === defaultRetryCount + 1) {
167+
response.end(fixture);
168+
} else {
169+
const twoSecondsByDelta = 2;
170+
const oneSecondByEpoch = (Date.now() / 1000) + 1;
171+
response.writeHead(429, {
172+
'RateLimit-Reset': (requestCount < 2) ? twoSecondsByDelta : oneSecondByEpoch,
173+
});
174+
175+
response.end('');
176+
}
177+
});
178+
179+
await withPerformance({
180+
t,
181+
expectedDuration: 2000 + 1000,
182+
async test() {
183+
t.is(await ky(server.url).text(), fixture);
184+
},
185+
});
186+
187+
t.is(requestCount, 3);
188+
189+
await server.close();
190+
});
191+
127192
test('respect 413 Retry-After', async t => {
128193
const startTime = Date.now();
129194
let requestCount = 0;

0 commit comments

Comments
 (0)
Please sign in to comment.