Skip to content

Commit f7cdda2

Browse files
committed
feat: enhance retry logic to handle additional retry headers parsing
1 parent 9802a11 commit f7cdda2

File tree

1 file changed

+49
-14
lines changed

1 file changed

+49
-14
lines changed

src/retry-handler.ts

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ import type { FetchResponse, RetryConfig, RetryFunction } from './types';
33
import { delayInvocation, timeNow } from './utils';
44
import { generateCacheKey } from './cache-manager';
55

6+
function getMsFromHttpDate(dateString: string): number | null {
7+
const ms = Date.parse(dateString) - timeNow();
8+
9+
if (!isNaN(ms)) {
10+
return Math.max(0, Math.floor(ms));
11+
}
12+
return null;
13+
}
14+
615
/**
716
* Calculates the number of milliseconds to wait before retrying a request,
817
* based on the `Retry-After` HTTP header in the provided response.
@@ -17,26 +26,52 @@ import { generateCacheKey } from './cache-manager';
1726
export function getRetryAfterMs(
1827
extendedResponse: FetchResponse | null,
1928
): number | null {
20-
const retryAfter = extendedResponse?.headers?.['retry-after'];
21-
22-
if (!retryAfter) {
29+
if (!extendedResponse) {
2330
return null;
2431
}
2532

26-
// Try parsing as seconds
27-
const seconds = Number(retryAfter);
33+
const headers = extendedResponse.headers || {};
34+
const retryAfter = headers['retry-after'];
35+
36+
if (retryAfter) {
37+
// Try parsing as seconds
38+
const seconds = Number(retryAfter);
39+
40+
if (!isNaN(seconds) && seconds >= 0) {
41+
return seconds * 1000;
42+
}
43+
44+
const ms = getMsFromHttpDate(retryAfter);
2845

29-
if (!isNaN(seconds) && seconds >= 0) {
30-
return seconds * 1000;
46+
if (ms !== null) {
47+
return ms;
48+
}
3149
}
3250

33-
// Try parsing as HTTP-date
34-
const date = new Date(retryAfter);
51+
// Headers are already in lowercase
52+
const RATELIMIT_RESET = 'ratelimit-reset';
53+
54+
// Unix timestamp when the rate limit window resets (relative to current time)
55+
// Fallback to checking 'ratelimit-reset-after' OR 'x-ratelimit-reset-after' headers
56+
const rateLimitResetAfter =
57+
headers[RATELIMIT_RESET + '-after'] ||
58+
headers['x-' + RATELIMIT_RESET + '-after'];
59+
60+
if (rateLimitResetAfter) {
61+
const seconds = Number(rateLimitResetAfter);
62+
63+
if (!isNaN(seconds)) {
64+
return seconds * 1000;
65+
}
66+
}
3567

36-
if (!isNaN(date.getTime())) {
37-
const ms = date.getTime() - timeNow();
68+
// ISO 8601 datetime when the rate limit resets
69+
// Fallback to checking 'ratelimit-reset-at' 'x-ratelimit-reset-at' headers
70+
const rateLimitResetAt =
71+
headers[RATELIMIT_RESET + '-at'] || headers['x-' + RATELIMIT_RESET + '-at'];
3872

39-
return ms > 0 ? ms : 0;
73+
if (rateLimitResetAt) {
74+
return getMsFromHttpDate(rateLimitResetAt);
4075
}
4176

4277
return null;
@@ -139,8 +174,8 @@ export async function withRetry<
139174
}
140175

141176
// If we should not stop retrying, continue to the next attempt
142-
// If the error status is 429 (Too Many Requests), handle rate limiting
143-
if (error.status === 429) {
177+
// Handle rate limiting if the error status is 429 (Too Many Requests) or 503 (Service Unavailable)
178+
if (error.status === 429 || error.status === 503) {
144179
// Try to extract the "Retry-After" value from the response headers
145180
const retryAfterMs = getRetryAfterMs(output);
146181

0 commit comments

Comments
 (0)