Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 28 additions & 12 deletions lib/request/fetch-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,15 +691,21 @@ interface RateLimitErrorBody {

function parseRateLimitBody(
body: string,
): { code?: string; resetsAt?: number; retryAfterMs?: number } | undefined {
): {
code?: string;
resetsAt?: number;
retryAfterMs?: number;
retryAfterSeconds?: number;
} | undefined {
if (!body) return undefined;
try {
const parsed = JSON.parse(body) as RateLimitErrorBody;
const error = parsed?.error ?? {};
const code = (error.code ?? error.type ?? "").toString();
const resetsAt = toNumber(error.resets_at ?? error.reset_at);
const retryAfterMs = toNumber(error.retry_after_ms ?? error.retry_after);
return { code, resetsAt, retryAfterMs };
const retryAfterMs = toNumber(error.retry_after_ms);
const retryAfterSeconds = toNumber(error.retry_after);
return { code, resetsAt, retryAfterMs, retryAfterSeconds };
} catch {
return undefined;
}
Expand Down Expand Up @@ -824,10 +830,18 @@ function ensureJsonErrorResponse(response: Response, payload: ErrorPayload): Res

function parseRetryAfterMs(
response: Response,
parsedBody?: { resetsAt?: number; retryAfterMs?: number },
parsedBody?: {
resetsAt?: number;
retryAfterMs?: number;
retryAfterSeconds?: number;
},
): number | null {
if (parsedBody?.retryAfterMs !== undefined) {
return normalizeRetryAfter(parsedBody.retryAfterMs);
return normalizeRetryAfterMilliseconds(parsedBody.retryAfterMs);
}

if (parsedBody?.retryAfterSeconds !== undefined) {
return normalizeRetryAfterSeconds(parsedBody.retryAfterSeconds);
}

const retryAfterMsHeader = response.headers.get("retry-after-ms");
Expand Down Expand Up @@ -881,18 +895,20 @@ function parseRetryAfterMs(
return null;
}

function normalizeRetryAfter(value: number): number {
function normalizeRetryAfterMilliseconds(value: number): number {
if (!Number.isFinite(value)) return 60000;
let ms: number;
if (value > 0 && value < 1000) {
ms = Math.floor(value * 1000);
} else {
ms = Math.floor(value);
}
const ms = Math.floor(value);
const MAX_RETRY_DELAY_MS = 5 * 60 * 1000;
return Math.min(ms, MAX_RETRY_DELAY_MS);
}

function normalizeRetryAfterSeconds(value: number): number {
if (!Number.isFinite(value)) return 60000;
const ms = Math.floor(value * 1000);
const MAX_RETRY_DELAY_MS = 5 * 60 * 1000;
return Math.min(ms, MAX_RETRY_DELAY_MS);
}

function toNumber(value: unknown): number | undefined {
if (value === null || value === undefined) return undefined;
const parsed = Number(value);
Expand Down
13 changes: 11 additions & 2 deletions test/fetch-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,13 +664,13 @@ describe('Fetch Helpers Module', () => {
expect(rateLimit?.retryAfterMs).toBeGreaterThan(0);
});

it('normalizes small retryAfterMs values as seconds', async () => {
it('keeps retry_after_ms values in milliseconds even when small', async () => {
const body = { error: { message: 'rate limited', retry_after_ms: 5 } };
const response = new Response(JSON.stringify(body), { status: 429 });

const { rateLimit } = await handleErrorResponse(response);

expect(rateLimit?.retryAfterMs).toBe(5000);
expect(rateLimit?.retryAfterMs).toBe(5);
});

it('caps retryAfterMs at 5 minutes', async () => {
Expand All @@ -691,6 +691,15 @@ describe('Fetch Helpers Module', () => {
expect(rateLimit?.retryAfterMs).toBe(60000);
});

it('treats retry_after as seconds from body payload', async () => {
const body = { error: { message: 'rate limited', retry_after: 5 } };
const response = new Response(JSON.stringify(body), { status: 429 });

const { rateLimit } = await handleErrorResponse(response);

expect(rateLimit?.retryAfterMs).toBe(5000);
});

it('handles millisecond unix timestamp in reset header', async () => {
const futureTimestampMs = Date.now() + 45000;
const headers = new Headers({ 'x-ratelimit-reset': String(futureTimestampMs) });
Expand Down