This package has been extracted from the original fbjs library and it exposes single fetchWithRetries
. This function is only a wrapper for any other well known fetch API. However, this fetch also solves two common problems:
- fetch timeouts, and
- retries
This makes the fetch function more suitable for real-life production usage because it doesn't hang or fail easily. In other words you are not going to have many open connections just because the API is slow (this could kill your server completely) and your fetch won't give up if the API didn't respond for the first time (just some glitch and one retry would fix it).
Installation
yarn add @adeira/fetch
Usage
This fetch is basically drop-in replacement for any other fetch you use:
import fetch from '@adeira/fetch';
(async () => {
const response = await fetch('https://api.skypicker.com/locations?term=Barcelona');
console.log(await response.json());
})();
There are however some interesting features on top of this API. You can for example change the internal timings (the defaults are good enough):
import fetchWithRetries from '@adeira/fetch';
(async () => {
try {
const response = await fetchWithRetries(
'https://cs-api.skypicker.com/public/numbers?country_code=er404',
{
fetchTimeout: 15000,
retryDelays: [1000, 3000],
},
);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error.response);
console.error(error.response.status);
console.error(error.response.statusText);
const response = await error.response.json();
console.error(response);
}
})();
It does two things:
- it will timeout the connection after 15 seconds, and
- it will retry after 1 second and then after 3 seconds
Retries are performed in these situations:
- original fetch request failed
- fetch returned HTTP status <200 or >=300 (but some not transitive HTTP status codes like 401 or 403 are ignored)
- when the timeout (
fetchTimeout
) occurs
This package uses fetch ponyfill internally (cross-fetch) so it supports server JS as well as browsers and React Native environment. It will always try to use global fetch
if available before using this ponyfill.
Error handling
You have to catch errors while fetching the response. This fetch throws these exceptions:
- standard fetch exception (
Error
) when request failed for some reason TimeoutError
when fetch fails because of defined timeoutResponseError
when final response returns HTTP status <200 or >=300
Example:
import fetchWithRetries, { TimeoutError, ResponseError } from '@adeira/fetch';
(async () => {
try {
const response = await fetchWithRetries('//localhost');
const data = await response.json();
console.log(data);
} catch (error) {
if (error instanceof TimeoutError) {
console.error('request timeouted');
} else if (error instanceof ResponseError) {
console.error('unsuccessful response', error.response);
} else {
console.error('unknown error');
}
}
})();
Frequently Asked Questions
How does the timing work?
Let's have a look at this config:
const config = {
fetchTimeout: 2000,
retryDelays: [100, 3000],
};
There are many situations that may occur (skipping happy path):
- request immediately failed with HTTP status code 500
- retries after 100ms
- request successful (we are done), OR fail again
- retries after 3000ms again for the last time
Example with timeouts:
- first request takes too long and it's terminated after 2000ms
- next retry was scheduled to be after 100ms but we already burned 2000ms so it's going to be executed immediately
- second request takes to long as well and is terminated after 2000ms
- last request will wait 3000ms minus the burned timeout => 1000ms
- last attempt (it will timeout or resolve correctly)
In reality you can see some more optimistic scenarios: for example request failed with HTTP status code 500 and it's resolved immediately after you retry it (just some API glitch). Similar scenarios are quite often for timeouts: first try timeouted for some reason but it's OK when you try for the second time. This makes this fetch much better than the commonly used alternatives - this fetch does not expect success all the time and it tries to handle these real-world scenarios.
How do I mock this fetch?
One way how to mock this fetch is to use manual mock (src/__mocks__/@adeira/fetch.js
):
export default function fetchWithRetriesMock(resource: string) {
return Promise.resolve(`MODIFIED ${resource.toUpperCase()} 1`);
}
And then just use it:
import fetchWithRetriesMock from '@adeira/fetch';
jest.mock('@adeira/fetch');
it('mocks the fetch', async () => {
await expect(fetchWithRetriesMock('input')).resolves.toBe('MODIFIED INPUT 1');
});
Alternatively, you could inline the mock:
import fetchWithRetriesMock from '@adeira/fetch';
jest.mock('@adeira/fetch', () => {
return resource => Promise.resolve(`MODIFIED ${resource.toUpperCase()} 2`);
});
it('mocks the fetch', async () => {
await expect(fetchWithRetriesMock('input')).resolves.toBe('MODIFIED INPUT 2');
});
Why mocking global.fetch
doesn't work? It's because this fetch doesn't use or pollute global
variable: it uses ponyfill instead of polyfill behind the scenes.