UNPKG

7.3 kBJavaScriptView Raw
1import isRetryAllowed from 'is-retry-allowed';
2
3const namespace = 'axios-retry';
4
5/**
6 * @param {Error} error
7 * @return {boolean}
8 */
9export function isNetworkError(error) {
10 return (
11 !error.response &&
12 Boolean(error.code) && // Prevents retrying cancelled requests
13 error.code !== 'ECONNABORTED' && // Prevents retrying timed out requests
14 isRetryAllowed(error)
15 ); // Prevents retrying unsafe errors
16}
17
18const SAFE_HTTP_METHODS = ['get', 'head', 'options'];
19const IDEMPOTENT_HTTP_METHODS = SAFE_HTTP_METHODS.concat(['put', 'delete']);
20
21/**
22 * @param {Error} error
23 * @return {boolean}
24 */
25export function isRetryableError(error) {
26 return (
27 error.code !== 'ECONNABORTED' &&
28 (!error.response || (error.response.status >= 500 && error.response.status <= 599))
29 );
30}
31
32/**
33 * @param {Error} error
34 * @return {boolean}
35 */
36export function isSafeRequestError(error) {
37 if (!error.config) {
38 // Cannot determine if the request can be retried
39 return false;
40 }
41
42 return isRetryableError(error) && SAFE_HTTP_METHODS.indexOf(error.config.method) !== -1;
43}
44
45/**
46 * @param {Error} error
47 * @return {boolean}
48 */
49export function isIdempotentRequestError(error) {
50 if (!error.config) {
51 // Cannot determine if the request can be retried
52 return false;
53 }
54
55 return isRetryableError(error) && IDEMPOTENT_HTTP_METHODS.indexOf(error.config.method) !== -1;
56}
57
58/**
59 * @param {Error} error
60 * @return {boolean | Promise}
61 */
62export function isNetworkOrIdempotentRequestError(error) {
63 return isNetworkError(error) || isIdempotentRequestError(error);
64}
65
66/**
67 * @return {number} - delay in milliseconds, always 0
68 */
69function noDelay() {
70 return 0;
71}
72
73/**
74 * @param {number} [retryNumber=0]
75 * @return {number} - delay in milliseconds
76 */
77export function exponentialDelay(retryNumber = 0) {
78 const delay = Math.pow(2, retryNumber) * 100;
79 const randomSum = delay * 0.2 * Math.random(); // 0-20% of the delay
80 return delay + randomSum;
81}
82
83/**
84 * Initializes and returns the retry state for the given request/config
85 * @param {AxiosRequestConfig} config
86 * @return {Object}
87 */
88function getCurrentState(config) {
89 const currentState = config[namespace] || {};
90 currentState.retryCount = currentState.retryCount || 0;
91 config[namespace] = currentState;
92 return currentState;
93}
94
95/**
96 * Returns the axios-retry options for the current request
97 * @param {AxiosRequestConfig} config
98 * @param {AxiosRetryConfig} defaultOptions
99 * @return {AxiosRetryConfig}
100 */
101function getRequestOptions(config, defaultOptions) {
102 return { ...defaultOptions, ...config[namespace] };
103}
104
105/**
106 * @param {Axios} axios
107 * @param {AxiosRequestConfig} config
108 */
109function fixConfig(axios, config) {
110 if (axios.defaults.agent === config.agent) {
111 delete config.agent;
112 }
113 if (axios.defaults.httpAgent === config.httpAgent) {
114 delete config.httpAgent;
115 }
116 if (axios.defaults.httpsAgent === config.httpsAgent) {
117 delete config.httpsAgent;
118 }
119}
120
121/**
122 * Checks retryCondition if request can be retried. Handles it's retruning value or Promise.
123 * @param {number} retries
124 * @param {Function} retryCondition
125 * @param {Object} currentState
126 * @param {Error} error
127 * @return {boolean}
128 */
129async function shouldRetry(retries, retryCondition, currentState, error) {
130 const shouldRetryOrPromise = currentState.retryCount < retries && retryCondition(error);
131
132 // This could be a promise
133 if (typeof shouldRetryOrPromise === 'object') {
134 try {
135 await shouldRetryOrPromise;
136 return true;
137 } catch (_err) {
138 return false;
139 }
140 }
141 return shouldRetryOrPromise;
142}
143
144/**
145 * Adds response interceptors to an axios instance to retry requests failed due to network issues
146 *
147 * @example
148 *
149 * import axios from 'axios';
150 *
151 * axiosRetry(axios, { retries: 3 });
152 *
153 * axios.get('http://example.com/test') // The first request fails and the second returns 'ok'
154 * .then(result => {
155 * result.data; // 'ok'
156 * });
157 *
158 * // Exponential back-off retry delay between requests
159 * axiosRetry(axios, { retryDelay : axiosRetry.exponentialDelay});
160 *
161 * // Custom retry delay
162 * axiosRetry(axios, { retryDelay : (retryCount) => {
163 * return retryCount * 1000;
164 * }});
165 *
166 * // Also works with custom axios instances
167 * const client = axios.create({ baseURL: 'http://example.com' });
168 * axiosRetry(client, { retries: 3 });
169 *
170 * client.get('/test') // The first request fails and the second returns 'ok'
171 * .then(result => {
172 * result.data; // 'ok'
173 * });
174 *
175 * // Allows request-specific configuration
176 * client
177 * .get('/test', {
178 * 'axios-retry': {
179 * retries: 0
180 * }
181 * })
182 * .catch(error => { // The first request fails
183 * error !== undefined
184 * });
185 *
186 * @param {Axios} axios An axios instance (the axios object or one created from axios.create)
187 * @param {Object} [defaultOptions]
188 * @param {number} [defaultOptions.retries=3] Number of retries
189 * @param {boolean} [defaultOptions.shouldResetTimeout=false]
190 * Defines if the timeout should be reset between retries
191 * @param {Function} [defaultOptions.retryCondition=isNetworkOrIdempotentRequestError]
192 * A function to determine if the error can be retried
193 * @param {Function} [defaultOptions.retryDelay=noDelay]
194 * A function to determine the delay between retry requests
195 */
196export default function axiosRetry(axios, defaultOptions) {
197 axios.interceptors.request.use((config) => {
198 const currentState = getCurrentState(config);
199 currentState.lastRequestTime = Date.now();
200 return config;
201 });
202
203 axios.interceptors.response.use(null, async (error) => {
204 const { config } = error;
205
206 // If we have no information to retry the request
207 if (!config) {
208 return Promise.reject(error);
209 }
210
211 const {
212 retries = 3,
213 retryCondition = isNetworkOrIdempotentRequestError,
214 retryDelay = noDelay,
215 shouldResetTimeout = false
216 } = getRequestOptions(config, defaultOptions);
217
218 const currentState = getCurrentState(config);
219
220 if (await shouldRetry(retries, retryCondition, currentState, error)) {
221 currentState.retryCount += 1;
222 const delay = retryDelay(currentState.retryCount, error);
223
224 // Axios fails merging this configuration to the default configuration because it has an issue
225 // with circular structures: https://github.com/mzabriskie/axios/issues/370
226 fixConfig(axios, config);
227
228 if (!shouldResetTimeout && config.timeout && currentState.lastRequestTime) {
229 const lastRequestDuration = Date.now() - currentState.lastRequestTime;
230 // Minimum 1ms timeout (passing 0 or less to XHR means no timeout)
231 config.timeout = Math.max(config.timeout - lastRequestDuration - delay, 1);
232 }
233
234 config.transformRequest = [(data) => data];
235
236 return new Promise((resolve) => setTimeout(() => resolve(axios(config)), delay));
237 }
238
239 return Promise.reject(error);
240 });
241}
242
243// Compatibility with CommonJS
244axiosRetry.isNetworkError = isNetworkError;
245axiosRetry.isSafeRequestError = isSafeRequestError;
246axiosRetry.isIdempotentRequestError = isIdempotentRequestError;
247axiosRetry.isNetworkOrIdempotentRequestError = isNetworkOrIdempotentRequestError;
248axiosRetry.exponentialDelay = exponentialDelay;
249axiosRetry.isRetryableError = isRetryableError;