UNPKG

6.68 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}
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 Object.assign({}, 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 * Adds response interceptors to an axios instance to retry requests failed due to network issues
123 *
124 * @example
125 *
126 * import axios from 'axios';
127 *
128 * axiosRetry(axios, { retries: 3 });
129 *
130 * axios.get('http://example.com/test') // The first request fails and the second returns 'ok'
131 * .then(result => {
132 * result.data; // 'ok'
133 * });
134 *
135 * // Exponential back-off retry delay between requests
136 * axiosRetry(axios, { retryDelay : axiosRetry.exponentialDelay});
137 *
138 * // Custom retry delay
139 * axiosRetry(axios, { retryDelay : (retryCount) => {
140 * return retryCount * 1000;
141 * }});
142 *
143 * // Also works with custom axios instances
144 * const client = axios.create({ baseURL: 'http://example.com' });
145 * axiosRetry(client, { retries: 3 });
146 *
147 * client.get('/test') // The first request fails and the second returns 'ok'
148 * .then(result => {
149 * result.data; // 'ok'
150 * });
151 *
152 * // Allows request-specific configuration
153 * client
154 * .get('/test', {
155 * 'axios-retry': {
156 * retries: 0
157 * }
158 * })
159 * .catch(error => { // The first request fails
160 * error !== undefined
161 * });
162 *
163 * @param {Axios} axios An axios instance (the axios object or one created from axios.create)
164 * @param {Object} [defaultOptions]
165 * @param {number} [defaultOptions.retries=3] Number of retries
166 * @param {boolean} [defaultOptions.shouldResetTimeout=false]
167 * Defines if the timeout should be reset between retries
168 * @param {Function} [defaultOptions.retryCondition=isNetworkOrIdempotentRequestError]
169 * A function to determine if the error can be retried
170 * @param {Function} [defaultOptions.retryDelay=noDelay]
171 * A function to determine the delay between retry requests
172 */
173export default function axiosRetry(axios, defaultOptions) {
174 axios.interceptors.request.use(config => {
175 const currentState = getCurrentState(config);
176 currentState.lastRequestTime = Date.now();
177 return config;
178 });
179
180 axios.interceptors.response.use(null, error => {
181 const config = error.config;
182
183 // If we have no information to retry the request
184 if (!config) {
185 return Promise.reject(error);
186 }
187
188 const {
189 retries = 3,
190 retryCondition = isNetworkOrIdempotentRequestError,
191 retryDelay = noDelay,
192 shouldResetTimeout = false
193 } = getRequestOptions(config, defaultOptions);
194
195 const currentState = getCurrentState(config);
196
197 const shouldRetry = retryCondition(error) && currentState.retryCount < retries;
198
199 if (shouldRetry) {
200 currentState.retryCount += 1;
201 const delay = retryDelay(currentState.retryCount, error);
202
203 // Axios fails merging this configuration to the default configuration because it has an issue
204 // with circular structures: https://github.com/mzabriskie/axios/issues/370
205 fixConfig(axios, config);
206
207 if (!shouldResetTimeout && config.timeout && currentState.lastRequestTime) {
208 const lastRequestDuration = Date.now() - currentState.lastRequestTime;
209 // Minimum 1ms timeout (passing 0 or less to XHR means no timeout)
210 config.timeout = Math.max(config.timeout - lastRequestDuration - delay, 1);
211 }
212
213 config.transformRequest = [data => data];
214
215 return new Promise(resolve => setTimeout(() => resolve(axios(config)), delay));
216 }
217
218 return Promise.reject(error);
219 });
220}
221
222// Compatibility with CommonJS
223axiosRetry.isNetworkError = isNetworkError;
224axiosRetry.isSafeRequestError = isSafeRequestError;
225axiosRetry.isIdempotentRequestError = isIdempotentRequestError;
226axiosRetry.isNetworkOrIdempotentRequestError = isNetworkOrIdempotentRequestError;
227axiosRetry.exponentialDelay = exponentialDelay;
228axiosRetry.isRetryableError = isRetryableError;