UNPKG

up-fetch

Version:

Advanced fetch client builder for typescript.

458 lines (408 loc) 12 kB
import { scheduler } from 'node:timers/promises' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { afterAll, afterEach, beforeAll, describe, expect, expectTypeOf, test, vi, } from 'vitest' import { up } from '..' const baseUrl = 'https://example.com' const server = setupServer() beforeAll(() => server.listen()) afterEach(() => server.resetHandlers()) afterAll(() => server.close()) describe('retry', () => { test('should call retry.when with the response', async () => { const upfetch = up(fetch, () => ({ baseUrl, reject: () => false, retry: { attempts: 1, delay: 100, when(ctx) { spy() if (ctx.response) { expect(ctx.response instanceof Response).toBe(true) } else { expect(ctx.error instanceof Error).toBe(true) } return false }, }, })) server.use( http.get(baseUrl, () => HttpResponse.json({ hello: 'world' }, { status: 500 }), ), ) const spy = vi.fn() await upfetch('/') expect(spy).toHaveBeenCalledTimes(1) }) test('should not call retry.attempts or retry.delay when retry.when returns false', async () => { const upfetch = up(fetch, () => ({ baseUrl, reject: () => false, retry: { when: () => false, attempts: attemptsSpy, delay: () => { delaySpy() return 0 }, }, })) server.use( http.get(baseUrl, () => HttpResponse.json({ hello: 'world' }, { status: 500 }), ), ) const attemptsSpy = vi.fn() const delaySpy = vi.fn() await upfetch('/') expect(attemptsSpy).not.toHaveBeenCalled() expect(delaySpy).not.toHaveBeenCalled() }) test('should call retry.attempts with the request when retry.when returns true', async () => { const upfetch = up(fetch, () => ({ baseUrl, reject: () => false, retry: { when: () => true, delay: 100, attempts({ request }) { spy() expectTypeOf(request).toEqualTypeOf<Request>() expect(request instanceof Request).toBe(true) return 0 }, }, })) server.use( http.get(baseUrl, () => HttpResponse.json({ hello: 'world' }, { status: 500 }), ), ) const spy = vi.fn() await upfetch('/') expect(spy).toHaveBeenCalledTimes(1) }) test('should not call retry.delay when retry.attempts returns 0', async () => { const upfetch = up(fetch, () => ({ baseUrl, reject: () => false, retry: { when: () => true, attempts: () => 0, delay: () => { spy() return 0 }, }, })) server.use( http.get(baseUrl, () => HttpResponse.json({ hello: 'world' }, { status: 500 }), ), ) const spy = vi.fn() await upfetch('/') expect(spy).toHaveBeenCalledTimes(0) }) test('should call retry.delay with the attempt number and response when retry.when returns true and retry.attempts returns more than 0', async () => { const upfetch = up(fetch, () => ({ baseUrl, reject: () => false, retry: { when: () => true, attempts: () => 1, delay({ attempt, response, error }) { spy() expectTypeOf(attempt).toEqualTypeOf<number>() expect(attempt).toBe(1) if (response) { expectTypeOf(response).toEqualTypeOf<Response>() } else { expectTypeOf(error).toEqualTypeOf<unknown>() } expect(response instanceof Response).toBe(true) return 0 }, }, })) server.use( http.get(baseUrl, () => HttpResponse.json({ hello: 'world' }, { status: 500 }), ), ) const spy = vi.fn() await upfetch('/') expect(spy).toHaveBeenCalledTimes(1) }) test('should retry `N = retry.attempts()` times when retry.when returns true', async () => { const upfetch = up(fetch, () => ({ baseUrl, reject: () => false, retry: { attempts: 1, delay: 100, when: () => true, }, })) const spy = vi.fn() server.use( http.get(baseUrl, async () => { spy() return HttpResponse.json({ hello: 'world' }, { status: 500 }) }), ) await upfetch('/') expect(spy).toHaveBeenCalledTimes(2) }) test('should allow upfetch.attempts to override up.attempts', async () => { const upfetch = up(fetch, () => ({ baseUrl, reject: () => false, retry: { when: () => true, delay: 100, attempts: 1, }, })) const spy = vi.fn() server.use( http.get(baseUrl, async () => { spy() return HttpResponse.json({ hello: 'world' }, { status: 500 }) }), ) await upfetch('/', { retry: { attempts: 2 }, }) // no retry expect(spy).toHaveBeenCalledTimes(3) }) test('should handle errors during attempts() function execution', async () => { const upfetch = up(fetch, () => ({ baseUrl, reject: () => false, retry: { when: () => true, delay: 100, attempts: () => { throw new Error('attempts error') }, }, })) server.use( http.get(baseUrl, async () => HttpResponse.json({}, { status: 500 })), ) let exec = 0 await upfetch('/').catch((error) => { exec++ expect(error.name).toBe('Error') }) expect(exec).toBe(1) }) test('should handle errors during retry.delay function execution', async () => { const upfetch = up(fetch, () => ({ baseUrl, reject: () => false, retry: { when: () => true, attempts: 1, delay: () => { throw new Error('delay error') }, }, })) const spy = vi.fn() server.use( http.get(baseUrl, async () => { spy() return HttpResponse.json({ hello: 'world' }, { status: 500 }) }), ) await expect(upfetch('/')).rejects.toThrow('delay error') expect(spy).toHaveBeenCalledTimes(1) // Only initial request, no retries due to error }) test('should not call onRetry when no retry is needed', async () => { const onRetrySpy = vi.fn() const upfetch = up(fetch, () => ({ baseUrl, onRetry: onRetrySpy, reject: () => false, retry: { when: () => false, attempts: 2, delay: 0, }, })) server.use( http.get(baseUrl, () => HttpResponse.json({}, { status: 500 })), ) await upfetch('/') expect(onRetrySpy).not.toHaveBeenCalled() }) test('should call onRetry before each retry attempt', async () => { const onRetrySpy = vi.fn() const upfetch = up(fetch, () => ({ baseUrl, onRetry: onRetrySpy, reject: () => false, retry: { when: () => true, attempts: 2, delay: 0, }, })) server.use( http.get(baseUrl, () => HttpResponse.json({}, { status: 500 })), ) await upfetch('/') expect(onRetrySpy).toHaveBeenCalledTimes(2) expect(onRetrySpy).toHaveBeenNthCalledWith(1, { attempt: 1, response: expect.any(Response), request: expect.any(Request), }) expect(onRetrySpy).toHaveBeenNthCalledWith(2, { attempt: 2, response: expect.any(Response), request: expect.any(Request), }) }) test('should execute up.onRequest, up.onRetry and upfetch.onRetry on each retry attempt', async () => { let exec = 0 const upfetch = up(fetch, () => ({ baseUrl, onRequest(request) { // calls 3 times ++exec }, onRetry(context) { // calls 2 times ++exec }, reject: () => false, retry: { when: () => true, attempts: 2, delay: 0, }, })) server.use( http.get(baseUrl, () => HttpResponse.json({}, { status: 500 })), ) await upfetch('/', { onRequest(request) { // calls 3 times ++exec }, onRetry(context) { // calls 2 times ++exec }, }) expect(exec).toBe(10) }) test('should abort retry immediately if signal controller aborts during retry delay', async () => { const controller = new AbortController() const upfetch = up(fetch, () => ({ baseUrl, reject: () => false, retry: { when: () => true, attempts: 2, delay: 1000, }, })) server.use( http.get(baseUrl, async () => HttpResponse.json({}, { status: 500 })), ) let exec = 0 const promise = upfetch('/', { signal: controller.signal }) await scheduler.wait(50) const now = Date.now() controller.abort() await promise.catch((error) => { exec++ expect(error.name).toBe('AbortError') }) expect(exec).toBe(1) // should not wait for the whole 1000ms delay expect(Date.now() - now).toBeLessThan(50) }) test('should allow retrying after an error', async () => { const spy = vi.fn() const upfetch = up(fetch, () => ({ baseUrl: 'https://does.not.exist/', retry: { attempts: 2, delay: 0, when: (ctx) => !!ctx.error, }, onRetry(context) { spy() }, })) await upfetch('/').catch(() => {}) expect(spy).toHaveBeenCalledTimes(2) }) test('should allow retrying after a timeout', async () => { const spy = vi.fn() server.use( http.get(baseUrl, async () => { await scheduler.wait(10000) return HttpResponse.json({}, { status: 200 }) }), ) const upfetch = up(fetch, () => ({ baseUrl, timeout: 50, retry: { attempts: 2, when: (ctx: any) => ctx.error?.name === 'TimeoutError', delay: 0, }, onRetry(context) { spy() }, })) await upfetch('/').catch(() => {}) expect(spy).toHaveBeenCalledTimes(2) }) // https://github.com/L-Blondy/up-fetch/issues/75 test('Should cleanup the error after a successul fetch', async () => { server.use( http.get(baseUrl, async () => { return HttpResponse.json({}, { status: 200 }) }), ) const upfetch = up(fetch, () => ({ baseUrl, })) let exec = 0 await upfetch('/', { onRequest() { if (++exec === 1) { throw new Error('Generate an error for the first retry') } }, retry: { when({ error, response }) { return !!error || !response?.ok }, attempts: 3, }, }).catch(() => {}) // first exec throws, second exec is ok expect(exec).toBe(2) }) })