index.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import isRetryAllowed from 'is-retry-allowed';
  2. export const namespace = 'axios-retry';
  3. export function isNetworkError(error) {
  4. const CODE_EXCLUDE_LIST = ['ERR_CANCELED', 'ECONNABORTED'];
  5. if (error.response) {
  6. return false;
  7. }
  8. if (!error.code) {
  9. return false;
  10. }
  11. // Prevents retrying timed out & cancelled requests
  12. if (CODE_EXCLUDE_LIST.includes(error.code)) {
  13. return false;
  14. }
  15. // Prevents retrying unsafe errors
  16. return isRetryAllowed(error);
  17. }
  18. const SAFE_HTTP_METHODS = ['get', 'head', 'options'];
  19. const IDEMPOTENT_HTTP_METHODS = SAFE_HTTP_METHODS.concat(['put', 'delete']);
  20. export function isRetryableError(error) {
  21. return (error.code !== 'ECONNABORTED' &&
  22. (!error.response || (error.response.status >= 500 && error.response.status <= 599)));
  23. }
  24. export function isSafeRequestError(error) {
  25. if (!error.config?.method) {
  26. // Cannot determine if the request can be retried
  27. return false;
  28. }
  29. return isRetryableError(error) && SAFE_HTTP_METHODS.indexOf(error.config.method) !== -1;
  30. }
  31. export function isIdempotentRequestError(error) {
  32. if (!error.config?.method) {
  33. // Cannot determine if the request can be retried
  34. return false;
  35. }
  36. return isRetryableError(error) && IDEMPOTENT_HTTP_METHODS.indexOf(error.config.method) !== -1;
  37. }
  38. export function isNetworkOrIdempotentRequestError(error) {
  39. return isNetworkError(error) || isIdempotentRequestError(error);
  40. }
  41. function noDelay() {
  42. return 0;
  43. }
  44. export function exponentialDelay(retryNumber = 0, _error = undefined, delayFactor = 100) {
  45. const delay = 2 ** retryNumber * delayFactor;
  46. const randomSum = delay * 0.2 * Math.random(); // 0-20% of the delay
  47. return delay + randomSum;
  48. }
  49. export const DEFAULT_OPTIONS = {
  50. retries: 3,
  51. retryCondition: isNetworkOrIdempotentRequestError,
  52. retryDelay: noDelay,
  53. shouldResetTimeout: false,
  54. onRetry: () => { },
  55. onMaxRetryTimesExceeded: () => { }
  56. };
  57. function getRequestOptions(config, defaultOptions) {
  58. return { ...DEFAULT_OPTIONS, ...defaultOptions, ...config[namespace] };
  59. }
  60. function setCurrentState(config, defaultOptions) {
  61. const currentState = getRequestOptions(config, defaultOptions || {});
  62. currentState.retryCount = currentState.retryCount || 0;
  63. currentState.lastRequestTime = currentState.lastRequestTime || Date.now();
  64. config[namespace] = currentState;
  65. return currentState;
  66. }
  67. function fixConfig(axiosInstance, config) {
  68. // @ts-ignore
  69. if (axiosInstance.defaults.agent === config.agent) {
  70. // @ts-ignore
  71. delete config.agent;
  72. }
  73. if (axiosInstance.defaults.httpAgent === config.httpAgent) {
  74. delete config.httpAgent;
  75. }
  76. if (axiosInstance.defaults.httpsAgent === config.httpsAgent) {
  77. delete config.httpsAgent;
  78. }
  79. }
  80. async function shouldRetry(currentState, error) {
  81. const { retries, retryCondition } = currentState;
  82. const shouldRetryOrPromise = (currentState.retryCount || 0) < retries && retryCondition(error);
  83. // This could be a promise
  84. if (typeof shouldRetryOrPromise === 'object') {
  85. try {
  86. const shouldRetryPromiseResult = await shouldRetryOrPromise;
  87. // keep return true unless shouldRetryPromiseResult return false for compatibility
  88. return shouldRetryPromiseResult !== false;
  89. }
  90. catch (_err) {
  91. return false;
  92. }
  93. }
  94. return shouldRetryOrPromise;
  95. }
  96. async function handleMaxRetryTimesExceeded(currentState, error) {
  97. if (currentState.retryCount >= currentState.retries)
  98. await currentState.onMaxRetryTimesExceeded(error, currentState.retryCount);
  99. }
  100. const axiosRetry = (axiosInstance, defaultOptions) => {
  101. const requestInterceptorId = axiosInstance.interceptors.request.use((config) => {
  102. setCurrentState(config, defaultOptions);
  103. return config;
  104. });
  105. const responseInterceptorId = axiosInstance.interceptors.response.use(null, async (error) => {
  106. const { config } = error;
  107. // If we have no information to retry the request
  108. if (!config) {
  109. return Promise.reject(error);
  110. }
  111. const currentState = setCurrentState(config, defaultOptions);
  112. if (await shouldRetry(currentState, error)) {
  113. currentState.retryCount += 1;
  114. const { retryDelay, shouldResetTimeout, onRetry } = currentState;
  115. const delay = retryDelay(currentState.retryCount, error);
  116. // Axios fails merging this configuration to the default configuration because it has an issue
  117. // with circular structures: https://github.com/mzabriskie/axios/issues/370
  118. fixConfig(axiosInstance, config);
  119. if (!shouldResetTimeout && config.timeout && currentState.lastRequestTime) {
  120. const lastRequestDuration = Date.now() - currentState.lastRequestTime;
  121. const timeout = config.timeout - lastRequestDuration - delay;
  122. if (timeout <= 0) {
  123. return Promise.reject(error);
  124. }
  125. config.timeout = timeout;
  126. }
  127. config.transformRequest = [(data) => data];
  128. await onRetry(currentState.retryCount, error, config);
  129. return new Promise((resolve) => {
  130. setTimeout(() => resolve(axiosInstance(config)), delay);
  131. });
  132. }
  133. await handleMaxRetryTimesExceeded(currentState, error);
  134. return Promise.reject(error);
  135. });
  136. return { requestInterceptorId, responseInterceptorId };
  137. };
  138. // Compatibility with CommonJS
  139. axiosRetry.isNetworkError = isNetworkError;
  140. axiosRetry.isSafeRequestError = isSafeRequestError;
  141. axiosRetry.isIdempotentRequestError = isIdempotentRequestError;
  142. axiosRetry.isNetworkOrIdempotentRequestError = isNetworkOrIdempotentRequestError;
  143. axiosRetry.exponentialDelay = exponentialDelay;
  144. axiosRetry.isRetryableError = isRetryableError;
  145. export default axiosRetry;