File size: 3,682 Bytes
bf48b89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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();
    });
});