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 mockFetchJson = jest.fn().mockRejectedValueOnce(errorMsg);
const expected = {
someField: 'someValue',
};
const mockFetchJson = jest
.fn()
.mockRejectedValueOnce(errorMsg)
.mockRejectedValueOnce(errorMsg)
.mockResolvedValueOnce(expected);
const mockFetch = jest.fn().mockResolvedValue({
status: 200,
ok: true,
@ -99,10 +106,69 @@ describe('fetch capi', () => {
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(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 {
await fetchCapi({ endpoint, accessToken });
await fetchCapi({ endpoint, accessToken }, retryDelay, retryLimit);
} 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';
export type FetchCapiParams = {
@ -15,29 +16,48 @@ const getDetails = async (response: Response) => {
}
};
const provideResponse = async (response: Response) => {
if (response.ok) {
return await response.json();
const provideResponse = async (response: Response, retryDelay: number, retryLimit: number, attempt: number = 0) => {
try {
if (response.ok) {
attempt++;
return await response.json();
}
return Promise.reject({
status: response.status,
statusText: response.statusText || undefined,
details: await getDetails(response),
});
} catch (ex) {
if (attempt === retryLimit) {
return Promise.reject(ex);
}
await delay(retryDelay);
return provideResponse(response, retryDelay, retryLimit, attempt);
}
return Promise.reject({
status: response.status,
statusText: response.statusText || undefined,
details: await getDetails(response),
});
};
const doFetch = async (param: FetchCapiParams) =>
fetch(param.endpoint, {
method: param.method || 'GET',
headers: {
'Content-Type': 'application/json;charset=utf-8',
Authorization: param.accessToken ? `Bearer ${param.accessToken}` : undefined,
'X-Request-ID': guid(),
},
body: param.body ? JSON.stringify(param.body) : undefined,
});
export const fetchCapi = async <T>(param: FetchCapiParams): Promise<T> => {
const response = await doFetch(param);
return await provideResponse(response);
const doFetch = async (param: FetchCapiParams, retryDelay: number, retryLimit: number, attempt: number = 0) => {
try {
attempt++;
return await fetch(param.endpoint, {
method: param.method || 'GET',
headers: {
'Content-Type': 'application/json;charset=utf-8',
Authorization: param.accessToken ? `Bearer ${param.accessToken}` : undefined,
'X-Request-ID': guid(),
},
body: param.body ? JSON.stringify(param.body) : undefined,
});
} catch (ex) {
if (attempt === retryLimit) {
return Promise.reject(ex);
}
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);
};