TD-768: Restore fetch with retry (#245)

This commit is contained in:
Ildar Galeev 2023-10-09 12:34:51 +03:00 committed by GitHub
parent 77151894df
commit ec028279f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 112 additions and 26 deletions

View File

@ -86,9 +86,16 @@ describe('fetch capi', () => {
} }
}); });
test('should catch json reject', async () => { test('should retry json reject', async () => {
const errorMsg = 'Read json error'; const errorMsg = 'Read json error';
const mockFetchJson = jest.fn().mockRejectedValueOnce(errorMsg); const expected = {
someField: 'someValue',
};
const mockFetchJson = jest
.fn()
.mockRejectedValueOnce(errorMsg)
.mockRejectedValueOnce(errorMsg)
.mockResolvedValueOnce(expected);
const mockFetch = jest.fn().mockResolvedValue({ const mockFetch = jest.fn().mockResolvedValue({
status: 200, status: 200,
ok: true, ok: true,
@ -99,10 +106,69 @@ describe('fetch capi', () => {
const endpoint = 'https://api.test.com/endpoint'; const endpoint = 'https://api.test.com/endpoint';
const accessToken = 'testToken'; const accessToken = 'testToken';
const retryDelay = 50;
const retryLimit = 10;
const result = await fetchCapi({ endpoint, accessToken }, retryDelay, retryLimit);
expect(result).toStrictEqual(expected);
expect(mockFetchJson).toHaveBeenCalledTimes(3);
});
test('should retry failed fetch requests', async () => {
const expected = {
someField: 'someValue',
};
const mockFetch = jest
.fn()
.mockRejectedValueOnce(new Error('TypeError: Failed to fetch'))
.mockRejectedValueOnce(new Error('TypeError: Failed to fetch'))
.mockResolvedValueOnce({
status: 200,
ok: true,
json: async () => expected,
});
global.fetch = mockFetch;
const endpoint = 'https://api.test.com/endpoint';
const accessToken = 'testToken';
const retryDelay = 50;
const retryLimit = 10;
const result = await fetchCapi({ endpoint, accessToken }, retryDelay, retryLimit);
expect(result).toStrictEqual(expected);
expect(mockFetch).toHaveBeenCalledTimes(3);
const requestInit = {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json;charset=utf-8',
'X-Request-ID': expect.any(String),
},
method: 'GET',
};
expect(mockFetch).toHaveBeenCalledWith(endpoint, requestInit);
expect(mockFetch).toHaveBeenCalledWith(endpoint, requestInit);
expect(mockFetch).toHaveBeenCalledWith(endpoint, requestInit);
});
test('should retry failed fetch requests based on config', async () => {
const expectedError = new Error('TypeError: Failed to fetch');
const mockFetch = jest.fn().mockRejectedValue(expectedError);
global.fetch = mockFetch;
const endpoint = 'https://api.test.com/endpoint';
const accessToken = 'testToken';
const retryDelay = 50;
const retryLimit = 10;
try { try {
await fetchCapi({ endpoint, accessToken }); await fetchCapi({ endpoint, accessToken }, retryDelay, retryLimit);
} catch (error) { } catch (error) {
expect(error).toStrictEqual(errorMsg); expect(error).toEqual(expectedError);
} }
expect(mockFetch).toHaveBeenCalledTimes(retryLimit);
}); });
}); });

View File

@ -1,3 +1,4 @@
import delay from 'checkout/utils/delay';
import guid from 'checkout/utils/guid'; import guid from 'checkout/utils/guid';
export type FetchCapiParams = { export type FetchCapiParams = {
@ -15,8 +16,10 @@ const getDetails = async (response: Response) => {
} }
}; };
const provideResponse = async (response: Response) => { const provideResponse = async (response: Response, retryDelay: number, retryLimit: number, attempt: number = 0) => {
try {
if (response.ok) { if (response.ok) {
attempt++;
return await response.json(); return await response.json();
} }
return Promise.reject({ return Promise.reject({
@ -24,10 +27,19 @@ const provideResponse = async (response: Response) => {
statusText: response.statusText || undefined, statusText: response.statusText || undefined,
details: await getDetails(response), details: await getDetails(response),
}); });
} catch (ex) {
if (attempt === retryLimit) {
return Promise.reject(ex);
}
await delay(retryDelay);
return provideResponse(response, retryDelay, retryLimit, attempt);
}
}; };
const doFetch = async (param: FetchCapiParams) => const doFetch = async (param: FetchCapiParams, retryDelay: number, retryLimit: number, attempt: number = 0) => {
fetch(param.endpoint, { try {
attempt++;
return await fetch(param.endpoint, {
method: param.method || 'GET', method: param.method || 'GET',
headers: { headers: {
'Content-Type': 'application/json;charset=utf-8', 'Content-Type': 'application/json;charset=utf-8',
@ -36,8 +48,16 @@ const doFetch = async (param: FetchCapiParams) =>
}, },
body: param.body ? JSON.stringify(param.body) : undefined, body: param.body ? JSON.stringify(param.body) : undefined,
}); });
} catch (ex) {
export const fetchCapi = async <T>(param: FetchCapiParams): Promise<T> => { if (attempt === retryLimit) {
const response = await doFetch(param); return Promise.reject(ex);
return await provideResponse(response); }
await delay(retryDelay);
return doFetch(param, retryDelay, retryLimit, attempt);
}
};
export const fetchCapi = async <T>(param: FetchCapiParams, retryDelay = 3000, retryLimit = 10): Promise<T> => {
const response = await doFetch(param, retryDelay, retryLimit);
return await provideResponse(response, retryDelay, retryLimit);
}; };