import { RequestError } from 'got'; import { getRetryAfter, wrapWithRetry } from './retry-after.ts'; function requestError( response: { statusCode?: number; headers?: Record; } | null = null, ) { const err = new RequestError('request error', {}, null as never); if (response) { (err as any).response = response; } return err; } describe('util/http/retry-after', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); describe('wrapWithRetry', () => { it('works', async () => { const task = vi.fn(() => Promise.resolve(42)); const res = await wrapWithRetry(task, 'foobar', () => null, 60); expect(res).toBe(42); expect(task).toHaveBeenCalledTimes(1); }); it('throws', async () => { const task = vi.fn(() => Promise.reject(new Error('error'))); await expect( wrapWithRetry(task, 'http://example.com', () => null, 60), ).rejects.toThrow('error'); expect(task).toHaveBeenCalledTimes(1); }); it('retries', async () => { const task = vi .fn() .mockRejectedValueOnce(new Error('error-1')) .mockRejectedValueOnce(new Error('error-2')) .mockResolvedValueOnce(42); const p = wrapWithRetry(task, 'http://example.com', () => 1, 60); await vi.advanceTimersByTimeAsync(2000); const res = await p; expect(res).toBe(42); expect(task).toHaveBeenCalledTimes(3); }); it('gives up after max retries', async () => { const task = vi .fn() .mockRejectedValueOnce('error-1') .mockRejectedValueOnce('error-2') .mockRejectedValueOnce('error-3') .mockRejectedValue('error-4'); const p = wrapWithRetry(task, 'http://example.com', () => 1, 60).catch( (err) => err, ); await vi.advanceTimersByTimeAsync(2000); await expect(p).resolves.toBe('error-3'); expect(task).toHaveBeenCalledTimes(3); }); it('gives up when delay exceeds maxRetryAfter', async () => { const task = vi.fn().mockRejectedValue('error'); const p = wrapWithRetry(task, 'http://example.com', () => 61, 60).catch( (err) => err, ); await expect(p).resolves.toBe('error'); expect(task).toHaveBeenCalledTimes(1); }); }); describe('getRetryAfter', () => { it('returns null for non-RequestError', () => { expect(getRetryAfter(new Error())).toBeNull(); }); it('returns null for RequestError without response', () => { expect(getRetryAfter(requestError())).toBeNull(); }); it('returns null for status other than 429', () => { const err = new RequestError('request-error', {}, null as never); (err as any).response = { statusCode: 302 }; expect(getRetryAfter(requestError({ statusCode: 302 }))).toBeNull(); }); it('returns null missing "retry-after" header', () => { expect( getRetryAfter(requestError({ statusCode: 429, headers: {} })), ).toBeNull(); }); it('returns null for non-integer "retry-after" header', () => { expect( getRetryAfter( requestError({ statusCode: 429, headers: { 'retry-after': 'Wed, 21 Oct 2015 07:28:00 GMT', }, }), ), ).toBeNull(); }); it('returns delay in seconds from date', () => { vi.setSystemTime(new Date('2020-01-01T00:00:00Z')); expect( getRetryAfter( requestError({ statusCode: 429, headers: { 'retry-after': 'Wed, 01 Jan 2020 00:00:42 GMT', }, }), ), ).toBe(42); }); it('returns delay in seconds from number', () => { expect( getRetryAfter( requestError({ statusCode: 429, headers: { 'retry-after': '42', }, }), ), ).toBe(42); }); it('returns null for invalid header value', () => { expect( getRetryAfter( requestError({ statusCode: 429, headers: { 'retry-after': 'invalid', }, }), ), ).toBeNull(); }); }); });