diff --git a/packages/js-lib/src/http/fetcher.test.ts b/packages/js-lib/src/http/fetcher.test.ts index 228bd24b..e0c9283c 100644 --- a/packages/js-lib/src/http/fetcher.test.ts +++ b/packages/js-lib/src/http/fetcher.test.ts @@ -377,6 +377,27 @@ test('retryAfter date', async () => { expect(r).toBe('ok') }) +test('retryAfter cloudflare body', async () => { + vi.useFakeTimers() + const fetcher = getFetcher({ + debug: true, + }) + + // Cloudflare returns retry-after:0 but encodes the real wait in the JSON body as retry_after + const badResponse = (): Response => + Response.json({ retry_after: 2 }, { status: 429, headers: { 'retry-after': '0' } }) + + vi.spyOn(Fetcher, 'callNativeFetch') + .mockResolvedValueOnce(badResponse()) + .mockResolvedValueOnce(badResponse()) + .mockResolvedValueOnce(new Response('ok')) + + const promise = fetcher.getText('') + await vi.runAllTimersAsync() + const r = await promise + expect(r).toBe('ok') +}) + test('tryFetch', async () => { vi.spyOn(Fetcher, 'callNativeFetch').mockResolvedValue( new Response('bad', { diff --git a/packages/js-lib/src/http/fetcher.ts b/packages/js-lib/src/http/fetcher.ts index caf50846..00f242cc 100644 --- a/packages/js-lib/src/http/fetcher.ts +++ b/packages/js-lib/src/http/fetcher.ts @@ -629,6 +629,15 @@ export class Fetcher { this.cfg.logger.warn('retry-after could not be parsed') } } + + // Cloudflare returns retry-after:0 but includes the real delay in the body as retry_after + if (!(timeout > 0) && res.body && typeof res.body === 'object') { + const bodyRetryAfter = (res.body as any).retry_after + if (typeof bodyRetryAfter === 'number' && bodyRetryAfter > 0) { + timeout = bodyRetryAfter * 1000 + this.cfg.logger.log(`retry-after (from body): ${bodyRetryAfter}`) + } + } } if (!timeout) {