import undici from 'undici'; import { afterEach, describe, expect, it, vi } from 'vitest'; const buildProxyState = () => [ { uri: 'http://proxy1.test', isActive: true, failureCount: 0, urlHandler: new URL('http://proxy1.test'), }, { uri: 'http://proxy2.test', isActive: true, failureCount: 0, urlHandler: new URL('http://proxy2.test'), }, ]; const loadWrappedFetch = async (proxyMock: any) => { vi.resetModules(); vi.doMock('@/utils/logger', () => ({ default: { debug: vi.fn(), warn: vi.fn(), info: vi.fn(), error: vi.fn(), http: vi.fn(), }, })); vi.doMock('@/utils/proxy', () => ({ default: proxyMock, })); return (await import('@/utils/request-rewriter/fetch')).default; }; afterEach(() => { vi.restoreAllMocks(); vi.resetModules(); vi.unmock('@/utils/logger'); vi.unmock('@/utils/proxy'); }); describe('request-rewriter fetch retry', () => { it('retries with the next proxy when prefer-proxy header is set', async () => { const proxies = buildProxyState(); let index = 0; const proxyMock = { proxyObj: { strategy: 'on_retry', url_regex: 'example.com', }, proxyUrlHandler: null, multiProxy: { allProxies: proxies, }, getCurrentProxy: vi.fn(() => proxies[index]), markProxyFailed: vi.fn(() => { index = 1; }), getDispatcherForProxy: vi.fn((proxyState) => ({ proxy: proxyState.uri, })), }; const wrappedFetch = await loadWrappedFetch(proxyMock); const fetchSpy = vi.spyOn(undici, 'fetch'); fetchSpy.mockRejectedValueOnce(new Error('boom')); fetchSpy.mockResolvedValueOnce(new Response('ok')); const response = await wrappedFetch('http://example.com/resource', { headers: new Headers({ 'x-prefer-proxy': '1', }), }); expect(response).toBeInstanceOf(Response); expect(fetchSpy).toHaveBeenCalledTimes(2); expect(proxyMock.markProxyFailed).toHaveBeenCalledWith('http://proxy1.test'); expect(proxyMock.getDispatcherForProxy).toHaveBeenCalledWith(proxies[1]); const requestArg = fetchSpy.mock.calls[0][0] as Request; expect(requestArg.headers.get('x-prefer-proxy')).toBeNull(); }); it('drops dispatcher when no next proxy is available', async () => { const proxies = buildProxyState(); const proxyMock = { proxyObj: { strategy: 'on_retry', url_regex: 'example.com', }, proxyUrlHandler: null, multiProxy: { allProxies: proxies, }, getCurrentProxy: vi.fn(() => proxies[0]), markProxyFailed: vi.fn(), getDispatcherForProxy: vi.fn((proxyState) => ({ proxy: proxyState.uri, })), }; const wrappedFetch = await loadWrappedFetch(proxyMock); const fetchSpy = vi.spyOn(undici, 'fetch'); fetchSpy.mockRejectedValueOnce(new Error('boom')); fetchSpy.mockResolvedValueOnce(new Response('ok')); await wrappedFetch('http://example.com/resource', { headers: { 'x-prefer-proxy': '1', }, }); expect(fetchSpy).toHaveBeenCalledTimes(2); expect(fetchSpy.mock.calls[1][1]?.dispatcher).toBeUndefined(); }); });