Skip to content

Commit 4be5743

Browse files
committed
Fix misleading error message for 503 long backoff and type safety
- isRateLimitExceeded: include 429 (was only catching 403), so the 'rate limit exceeded' message correctly covers all HTTP rate-limit responses while 503 long-backoff gets its own message - Long-wait error message differentiates 429/403 rate limit from 503 server backoff: 'GitHub API requested a long backoff before retrying.' - options.signal: replace unsafe cast (as AbortSignal | undefined) with nullish coalescing (?? undefined) to preserve null-safety - Test callback: updated to async to match Promise<void> contract
1 parent 470f3c0 commit 4be5743

2 files changed

Lines changed: 11 additions & 5 deletions

File tree

src/api-utils.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ describe("fetchWithRetry – 403 rate-limit handling", () => {
250250
return new Response("ok", { status: 200 });
251251
}) as typeof fetch;
252252

253-
const res = await fetchWithRetry("https://example.com", {}, 3, (ms) => {
253+
const res = await fetchWithRetry("https://example.com", {}, 3, async (ms) => {
254254
callbackArgs.push(ms);
255255
});
256256
expect(res.status).toBe(200);

src/api-utils.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export function formatRetryWait(ms: number): string {
3737
* (see https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api)
3838
*/
3939
function isRateLimitExceeded(res: Response): boolean {
40+
// 429 Too Many Requests is always a rate-limit response.
41+
if (res.status === 429) return true;
42+
// 403 is a GitHub-specific rate-limit when accompanied by the right headers.
4043
if (res.status !== 403) return false;
4144
return (
4245
res.headers.get("x-ratelimit-remaining") === "0" || res.headers.get("Retry-After") !== null
@@ -145,9 +148,12 @@ export async function fetchWithRetry(
145148
// Cancel the response body before waiting/throwing to allow connection reuse
146149
await res.body?.cancel();
147150
if (!onRateLimit || longWaitAttempts >= maxRetries) {
148-
throw new Error(
149-
`GitHub API rate limit exceeded. Please retry in ${formatRetryWait(baseDelayMs)}.`,
150-
);
151+
// Produce an accurate message: 403/429 are rate-limit errors; 503 is a
152+
// server backoff that should not be labelled "rate limit exceeded".
153+
const messagePrefix = isRateLimitExceeded(res)
154+
? "GitHub API rate limit exceeded."
155+
: "GitHub API requested a long backoff before retrying.";
156+
throw new Error(`${messagePrefix} Please retry in ${formatRetryWait(baseDelayMs)}.`);
151157
}
152158
// Fix: await the callback — it owns the sleep so it can show a countdown
153159
// and honour an AbortSignal. Long waits do NOT count against maxRetries;
@@ -163,7 +169,7 @@ export async function fetchWithRetry(
163169
// Cancel the response body to allow the connection to be reused
164170
await res.body?.cancel();
165171
// Honour the caller's AbortSignal during the short wait
166-
await abortableDelay(delayMs, options.signal as AbortSignal | undefined);
172+
await abortableDelay(delayMs, options.signal ?? undefined);
167173
attempt++;
168174
}
169175
}

0 commit comments

Comments
 (0)