Apache License 2.0

技术解读

以下内容由 GitRead 中文社区 编辑团队基于项目 README 与官方资料整理, 结合中文开发者的常见使用场景补充技术选型建议。具体以项目官方文档为准。

鲜为人知的重试利器:retry-axios

项目背景与痛点

在现代 Web 应用中,HTTP 接口的健壮性至关重要。一个微小的网络抖动或服务器错误都可能影响用户体验,破坏用户体验。为此,许多开发者在自己的项目中实现了一些冗余机制,比如重试机制,在请求失败后重新发送请求,直到成功。这既复杂又容易出现重复性代码,因此一个灵活的、可复用的重试机制非常有用。retry-axios就是为了解决这一痛点而诞生。

Axios 作为 JavaScript 中最流行的 HTTP 客户端之一,拥有庞大的用户和广泛的使用场景。然而,它的官方版本并未内置重试机制,导致用户需要在不希望添加大量重复代码的情况下自行实现。retry-axios通过一系列精心设计的拦截器,巧妙地解决了这个问题,提供了高度灵活、可配置的重试功能。

核心技术揭秘

retry-axios 本质上是一个针对 Axios 的拦截器。它在 Axios 的请求链中插入自定义逻辑,允许用户设置各种重试策略,例如固定重试次数、指数退避等。下面通过 Mermaid 架构图来说明其工作原理:

graph TD A[用户发起 Axios 请求] --> B{"是否失败"} B -->|"是" --> C[判断是否超出重试次数] C -->|"是"| D[返回最终错误] C -->|"否"| E[等待重试间隔] E --> F[重新发送请求] B -->|"否"| G[响应正常]

架构设计与实现原理

当用户发起一个 Axios 请求时,retry-axios 会在 Axios 的内部拦截器中捕获该请求。如果请求失败(例如出现网络错误或服务器错误),它会检查是否已超出预设的重试次数。如果没有超出,它将等待一段时间(通常是指数级退避)后重新发送请求;如果超出,则直接返回最终错误信息。

核心的重试策略通过配置对象进行自定义,比如设置最大重试次数、重试间隔基数、是否处理服务器返回的错误码等。这种设计使得开发者能够根据具体需求灵活配置重试策略。

实现原理

  • 透明性:由于retry-axios 是 Axios 的拦截器,它不改变原有的 Axios API,这保证了透明性和易用性。
  • 灵活性:允许在每个请求上动态配置重试逻辑,为开发者提供了极大的灵活性。
  • 可定制性:可以自定义重试策略的每一步动作,通过定制拦截器,可以实现更复杂的功能。

功能亮点与差异

相比于其他重试方案,retry-axios最大的亮点在于其高度灵活的配置选项及针对 Axios 的高度集成。许多开发者在实现重试机制时,往往需要自己控制状态和重试逻辑,大大增加了代码的复杂性和维护成本。而通过retry-axios,这一过程变得现代化和模块化。

此外,retry-axios还支持多种重试策略,除了基本的固定重试外,还支持指数退避策略,为不同场景提供了更好的适应性和鲁棒性。

应用场景与落地建议

在实际生产环境中使用retry-axios需要一些注意事项:

  • 性能考量:在重试策略上需要适当平衡,避免重试次数过多影响整体性能。
  • 错误探测:合理配置哪类错误触发重试,而不是对所有错误都盲目重试,这可以有效避免无效请求浪费资源。

retry-axios适用于对网络请求有较高鲁棒性要求的应用场景,尤其适合金融交易、数据同步等场景。

综合评价

优点:

  • 高度集成:无缝集成到 Axios 请求链中,提供顺滑的用户体验。
  • 灵活配置:丰富的配置选项让开发者能够轻松定制重试行为。
  • 简洁易用:仅需数行代码即可引入重试功能。

缺点:

  • 缺乏高级功能:虽然内置了各种策略,但对于更复杂的需求,可能需要开发者自己实现。

总体而言,retry-axios是一个功能强大且易于使用的错误重试解决方案,值得高度推荐。通过它,开发者能够快速实现可靠的重试机制,这在当今复杂多变的网络环境中显得尤为重要。

关于

GitHub 原始描述(英文)

🦖 A super flexible interceptor for Axios that makes it easy to retry requests.

描述取自 GitHub 官方 API,保持仓库作者书写的原文。

简要分析

热度分
646
价值分
326
活跃状态
活跃

语言占比

JavaScript
TypeScript

Release

README

retry-axios

Use Axios interceptors to automatically retry failed requests. Super flexible. Built in exponential backoff.

retry-axios

NPM Version GitHub Actions codecov Biome

Installation

npm install retry-axios

CDN

For front-end applications, you can also use retry-axios directly from a CDN without a build step. This is useful for quick prototypes or environments where you can’t use npm.

jsDelivr

<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/src/index.js"></script>
<script>
  // Attach retry-axios to the global axios object
  rax.attach();

  // Now you can use axios with retry capabilities
  axios.get('https://httpbin.org/status/503')
    .then(response => {
      console.log('Success:', response.data);
    })
    .catch(error => {
      console.error('Error:', error.message);
    });
</script>

unpkg

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/[email protected]/build/src/index.js"></script>
<script>
  // Attach retry-axios to the global axios object
  rax.attach();

  // Now you can use axios with retry capabilities
  axios.get('https://httpbin.org/status/503')
    .then(response => {
      console.log('Success:', response.data);
    })
    .catch(error => {
      console.error('Error:', error.message);
    });
</script>

Usage

To use this library, import it alongside of axios:

// Just import rax and your favorite version of axios
const rax = require('retry-axios');
const axios = require('axios');

Or, if you’re using TypeScript / es modules:

import * as rax from 'retry-axios';
import axios from 'axios';

You can attach to the global axios object, and retry 3 times by default:

const interceptorId = rax.attach();
const res = await axios('https://test.local');

Or you can create your own axios instance to make scoped requests:

const myAxiosInstance = axios.create();
myAxiosInstance.defaults.raxConfig = {
  retry: 3
};
const interceptorId = rax.attach(myAxiosInstance);
const res = await myAxiosInstance.get('https://test.local');

You have a lot of options…

const interceptorId = rax.attach();
const res = await axios({
  url: 'https://test.local',
  raxConfig: {
    // Retry 3 times before giving up. Applies to all errors (5xx, network errors, timeouts, etc). Defaults to 3.
    retry: 3,

    // Milliseconds to delay between retries. Defaults to 100.
    // - For 'static': Fixed delay between retries
    // - For 'exponential': Base multiplier for exponential calculation
    // - For 'linear': Ignored (uses attempt * 1000)
    retryDelay: 100,

    // HTTP methods to automatically retry.  Defaults to:
    // ['GET', 'HEAD', 'OPTIONS', 'DELETE', 'PUT']
    httpMethodsToRetry: ['GET', 'HEAD', 'OPTIONS', 'DELETE', 'PUT'],

    // The response status codes to retry.  Supports a double
    // array with a list of ranges.  Defaults to:
    // [[100, 199], [429, 429], [500, 599]]
    statusCodesToRetry: [[100, 199], [429, 429], [500, 599]],

    // You can set the backoff type.
    // options are 'exponential' (default), 'static' or 'linear'
    backoffType: 'exponential',

    // Jitter strategy for exponential backoff. Defaults to 'none'.
    // Options: 'none', 'full', 'equal'
    // Helps prevent thundering herd in distributed systems
    jitter: 'full',

    // You can detect when an error occurs, before the backoff delay
    onError: async (err) => {
      const cfg = rax.getConfig(err);
      console.log(`Error occurred, retry attempt #${cfg.currentRetryAttempt + 1} will happen after backoff`);
    },

    // You can detect when a retry attempt is about to be made, after the backoff delay
    onRetryAttempt: async (err) => {
      const cfg = rax.getConfig(err);
      console.log(`Retry attempt #${cfg.currentRetryAttempt} is about to start`);
      console.log(`Retries remaining: ${cfg.retriesRemaining}`);

      // Check if this is the final retry attempt
      if (cfg.retriesRemaining === 0) {
        console.log('This is the final retry attempt');
      }
    }
  }
});

Backoff Types and Timing

The backoffType option controls how delays between retry attempts are calculated. There are three strategies available:

Exponential Backoff (default)

Uses the formula: ((2^attempt - 1) / 2) * retryDelay milliseconds

The retryDelay parameter (defaults to 100ms) is used as the base multiplier for the exponential calculation.

Example timing with default retryDelay: 100:

  • Retry 1: 50ms delay
  • Retry 2: 150ms delay
  • Retry 3: 350ms delay
  • Retry 4: 750ms delay
  • Retry 5: 1,550ms delay

Example timing with retryDelay: 1000:

  • Retry 1: 500ms delay
  • Retry 2: 1,500ms delay
  • Retry 3: 3,500ms delay
  • Retry 4: 7,500ms delay
  • Retry 5: 15,500ms delay
raxConfig: {
  backoffType: 'exponential',  // This is the default
  retryDelay: 1000,  // Use 1000ms as the base multiplier
  retry: 5
}
Static Backoff

Uses a fixed delay specified by retryDelay (defaults to 100ms if not set).

Example timing with retryDelay: 3000:

  • Retry 1: 3,000ms delay
  • Retry 2: 3,000ms delay
  • Retry 3: 3,000ms delay
raxConfig: {
  backoffType: 'static',
  retryDelay: 3000,  // 3 seconds between each retry
  retry: 3
}
Linear Backoff

Delay increases linearly: attempt * 1000 milliseconds

The retryDelay option is ignored when using linear backoff.

Example timing for the first 5 retries:

  • Retry 1: 1,000ms delay
  • Retry 2: 2,000ms delay
  • Retry 3: 3,000ms delay
  • Retry 4: 4,000ms delay
  • Retry 5: 5,000ms delay
raxConfig: {
  backoffType: 'linear',
  retry: 5
}
Maximum Retry Delay

You can cap the maximum delay for any backoff type using maxRetryDelay:

raxConfig: {
  backoffType: 'exponential',
  maxRetryDelay: 5000,  // Never wait more than 5 seconds
  retry: 10
}
Jitter

Jitter adds randomness to exponential backoff delays to prevent the “thundering herd” problem where many clients retry at the same time. This is especially useful in distributed systems.

Available jitter strategies (only applies to exponential backoff):

No Jitter (default)

raxConfig: {
  backoffType: 'exponential',
  jitter: 'none',  // or omit this option
  retryDelay: 1000
}
// Retry 1: exactly 500ms
// Retry 2: exactly 1,500ms
// Retry 3: exactly 3,500ms

Full Jitter

Randomizes the delay between 0 and the calculated exponential backoff:

raxConfig: {
  backoffType: 'exponential',
  jitter: 'full',
  retryDelay: 1000
}
// Retry 1: random between 0-500ms
// Retry 2: random between 0-1,500ms
// Retry 3: random between 0-3,500ms

Equal Jitter

Uses half fixed delay, half random:

raxConfig: {
  backoffType: 'exponential',
  jitter: 'equal',
  retryDelay: 1000
}
// Retry 1: 250ms + random(0-250ms) = 250-500ms
// Retry 2: 750ms + random(0-750ms) = 750-1,500ms
// Retry 3: 1,750ms + random(0-1,750ms) = 1,750-3,500ms

Recommendation: Use 'full' jitter for most distributed systems to minimize collision probability while maintaining good retry timing.

Callback Timing

There are two callbacks you can use to hook into the retry lifecycle:

  • onError: Called immediately when an error occurs, before the backoff delay. Use this for logging errors or performing actions that need to happen right away.
  • onRetryAttempt: Called after the backoff delay, just before the retry request is made. Use this for actions that need to happen right before retrying (like refreshing tokens).

Both functions are asynchronous and must return a promise. The retry will wait for the promise to resolve before proceeding. If the promise is rejected, the retry will be aborted:

const res = await axios({
  url: 'https://test.local',
  raxConfig: {
    onError: async (err) => {
      // Called immediately when error occurs
      console.log('An error occurred, will retry after backoff');
    },
    onRetryAttempt: async (err) => {
      // Called after backoff delay, before retry
      const token = await refreshToken(err);
      window.localStorage.setItem('token', token);
      // If refreshToken throws or this promise rejects,
      // the retry will be aborted
    }
  }
});

Tracking Retry Progress

You can track the current retry state using properties available in the configuration:

  • currentRetryAttempt: The number of retries that have been attempted (starts at 0, increments with each retry)
  • retriesRemaining: The number of retries left before giving up (calculated as retry - currentRetryAttempt)

These properties are particularly useful when you want to show different messages or take different actions based on whether this is the final retry attempt:

const res = await axios({
  url: 'https://test.local',
  raxConfig: {
    retry: 3,
    onRetryAttempt: async (err) => {
      const cfg = rax.getConfig(err);

      console.log(`Retry attempt ${cfg.currentRetryAttempt} of ${cfg.retry}`);
      console.log(`${cfg.retriesRemaining} retries remaining`);

      // Show user-facing error only on final retry
      if (cfg.retriesRemaining === 0) {
        showErrorNotification('Request failed after multiple attempts');
      }
    }
  }
});

This is especially useful when chaining retry-axios with other error interceptors:

// Global error handler that shows notifications
axios.interceptors.response.use(null, async (error) => {
  const cfg = rax.getConfig(error);

  // Only show error notification on the final retry attempt
  // Don't spam the user with notifications for intermediate failures
  if (cfg?.retriesRemaining === 0) {
    showUserNotification('An error occurred: ' + error.message);
  }

  return Promise.reject(error);
});

// Attach retry interceptor
rax.attach();

Customizing Retry Logic

You can customize which errors should trigger a retry using the shouldRetry function:

const res = await axios({
  url: 'https://test.local',
  raxConfig: {
    retry: 3,
    // Custom logic to decide if a request should be retried
    // This is called AFTER checking the retry count limit
    shouldRetry: err => {
      const cfg = rax.getConfig(err);

      // Don't retry on 4xx errors except 429
      if (err.response?.status && err.response.status >= 400 && err.response.status < 500) {
        return err.response.status === 429;
      }

      // Retry on network errors and 5xx errors
      return true;
    }
  }
});

If you want to add custom retry logic without duplicating too much of the built-in logic, rax.shouldRetryRequest will tell you if a request would normally be retried:

const res = await axios({
  url: 'https://test.local',
  raxConfig: {
    // Override the decision making process on if you should retry
    shouldRetry: err => {
      const cfg = rax.getConfig(err);
      if (cfg.currentRetryAttempt >= cfg.retry) return false // ensure max retries is always respected

      // Always retry this status text, regardless of code or request type
      if (err.response.statusText.includes('Try again')) return true

      // Handle the request based on your other config options, e.g. `statusCodesToRetry`
      return rax.shouldRetryRequest(err)
    }
  }
});

Accessing All Retry Errors

When retries are exhausted and the request finally fails, you can access the complete history of all errors that occurred during the retry attempts. This is particularly useful for debugging and understanding what went wrong, especially for non-idempotent operations like POST requests where the error may change between attempts.

try {
  await axios.post('https://test.local/api/endpoint', data, {
    raxConfig: {
      httpMethodsToRetry: ['POST'],
      retry: 3
    }
  });
} catch (err) {
  const cfg = rax.getConfig(err);

  // Access all errors encountered during retries
  if (cfg?.errors) {
    console.log(`Total attempts: ${cfg.errors.length}`);
    console.log(`First error: ${cfg.errors[0].response?.status}`);
    console.log(`Last error: ${err.response?.status}`);

    // Log all error details
    cfg.errors.forEach((error, index) => {
      console.log(`Attempt ${index + 1}: ${error.response?.status} - ${error.response?.data}`);
    });
  }
}

The errors array is automatically populated and contains:

  • First element: The initial error that triggered the retry logic
  • Subsequent elements: Errors from each retry attempt
  • Order: Errors are in chronological order (oldest to newest)

This feature is especially valuable when:

  • Debugging complex failure scenarios where errors change between attempts
  • Implementing custom error handling logic that needs to consider all failures
  • Logging and monitoring to understand the full context of request failures
  • Working with non-idempotent operations where side effects may occur

What Gets Retried

By default, retry-axios will retry requests that:

  1. Return specific HTTP status codes: 1xx (informational), 429 (too many requests), and 5xx (server errors)
  2. Are network errors without a response: ETIMEDOUT, ENOTFOUND, ECONNABORTED, ECONNRESET, etc.
  3. Use idempotent HTTP methods: GET, HEAD, PUT, OPTIONS, DELETE

The retry config option controls the maximum number of retry attempts for all error types. If you need different behavior for network errors vs response errors, use the shouldRetry function to implement custom logic.

How it works

This library attaches an interceptor to an axios instance you pass to the API. This way you get to choose which version of axios you want to run, and you can compose many interceptors on the same request pipeline.

License

Apache-2.0

评论

暂无评论