Skip to content

Instantly share code, notes, and snippets.

@mjstrasser mjstrasser/package.json
Last active Apr 3, 2018

Embed
What would you like to do?
{
"name": "retry-axios",
"version": "1.0.0",
"description": "Retry function for axios promise-based HTTP client",
"scripts": {
"test": "jest --coverage"
},
"dependencies": {
"is-retry-allowed": "^1.1.0"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.6.1",
"eslint": "^4.19.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-import": "^2.9.0",
"jest": "^22.4.2"
}
}
import isRetryAllowed from 'is-retry-allowed';
/**
* Determines if the error is a network error. Thanks to https://github.com/softonic/axios-retry.
*/
const isNetworkError = error => (
!error.response
&& error.code
&& Boolean(error.code)
&& error.code !== 'ECONNABORTED'
&& isRetryAllowed(error)
);
const isServerError = error => (
error.response
&& error.response.status >= 500
&& error.response.status <= 599
);
const isRetriable = error => isNetworkError(error) || isServerError(error);
const sleep = delay => new Promise(resolve => setTimeout(resolve, delay));
/**
* Retry axios calls if the service fails with a transient error.
*
* @param delays an ES6 iterable (e.g. array)
* @param axiosFunc an axios function to call
* @param axiosArgs arguments to the axios function
*/
export const retryAxios = async (delays, axiosFunc, ...axiosArgs) => { // eslint-disable-line consistent-return
const iterator = delays[Symbol.iterator]();
while (true) { // eslint-disable-line no-constant-condition
try {
return await axiosFunc(...axiosArgs); // eslint-disable-line no-await-in-loop
} catch (error) {
const { done, value } = iterator.next();
if (!done && isRetriable(error)) {
await sleep(value); // eslint-disable-line no-await-in-loop
} else {
throw error;
}
}
}
};
/**
* Iterable that generates sequences of integers, increasing exponentially.
*/
export class ExponentialDelays {
/**
* Constructs an iterable from a starting value and number to return.
*
* @param initialDelay value of first delay to return
* @param retryCount number of values to return
*/
constructor(initialDelay, retryCount) {
this.initialDelay = Math.max(1, initialDelay);
this.retryCount = Math.max(0, retryCount);
}
/**
* Generator that returns values as constructed.
*/
* iterator() {
let delay = this.initialDelay;
for (let retry = 0; retry < this.retryCount; retry += 1) {
yield delay;
delay *= 2;
}
}
/**
* Implements the ES6 iterable protocol by returning the iterator.
*/
[Symbol.iterator]() {
return this.iterator();
}
}
import { retryAxios, ExponentialDelays } from './retryAxios';
// Basic HTTP error class.
class HttpError extends Error {
constructor(response, ...params) {
super(...params);
this.response = response;
}
}
// Basic network error class.
class NetError extends Error {
constructor(code, ...params) {
super(...params);
this.code = code;
}
}
// Mocks a single request:
// - if retVal is an integer it becomes an HTTP return code;
// - else it becomes a network error code.
const mockRequest = async (retVal) => {
if (Number.isInteger(retVal)) {
// HTTP response or failure, according to status code.
const response = { status: retVal };
if (retVal >= 400 && retVal <= 599) {
throw new HttpError(response);
}
return response;
}
// Otherwise network error.
throw new NetError(retVal);
};
// Returns a function that calls mockRequest() for each specified value
// in the rest parameter.
const mockAxios = (...returns) => {
const iter = returns[Symbol.iterator]();
return () => {
const { done, value } = iter.next();
if (done) {
throw new Error('Testing error: mockAxios iterator done');
}
return mockRequest(value);
};
};
test('retryAxios should return successful requests', async () => {
expect.assertions(1);
const data = await retryAxios([5, 5], mockAxios(200));
expect(data.status).toBe(200);
});
test('retryAxios should throw client errors', async () => {
expect.assertions(1);
try {
await retryAxios([5, 5], mockAxios(400));
} catch (e) {
expect(e.response.status).toBe(400);
}
});
test('retryAxios should throw non-transient network errors', async () => {
expect.assertions(1);
try {
await retryAxios([5, 5], mockAxios('CERT_REJECTED'));
} catch (e) {
expect(e.code).toBe('CERT_REJECTED');
}
});
test('retryAxios should call once even without retries', async () => {
expect.assertions(1);
const data = await retryAxios([], mockAxios(200));
expect(data.status).toBe(200);
});
test('retryAxios should return first successful call if within retry limit', async () => {
let data;
expect.assertions(2);
data = await retryAxios([5], mockAxios(500, 200));
expect(data.status).toBe(200);
data = await retryAxios([5, 5, 5, 5], mockAxios('ETIMEDOUT', 500, 'ECONNRESET', 500, 301));
expect(data.status).toBe(301);
});
test('retryAxios should throw the last error if all retries have been exhausted', async () => {
expect.assertions(1);
try {
await retryAxios(new ExponentialDelays(5, 2), mockAxios(500, 'ECONNRESET', 500, 200));
} catch (e) {
expect(e.response.status).toBe(500);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.