Skip to content

Commit

Permalink
TD-768: Restore fetch with retry (#245)
Browse files Browse the repository at this point in the history
  • Loading branch information
KeinAsylum authored Oct 9, 2023
1 parent 7715189 commit ec02827
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 25 deletions.
74 changes: 70 additions & 4 deletions src/app/backend/fetch-capi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
});
});
62 changes: 41 additions & 21 deletions src/app/backend/fetch-capi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import delay from 'checkout/utils/delay';
import guid from 'checkout/utils/guid';

export type FetchCapiParams = {
Expand All @@ -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,
});
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): Promise<T> => {
const response = await doFetch(param);
return await provideResponse(response);
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);
};

0 comments on commit ec02827

Please sign in to comment.