使用 JavaScript 实现带有重试能力的请求工具函数

问题

请使用 JavaScript 封装一个请求工具函数:请求失败时自动重试,支持最大重试次数、指数退避,并避免无限等待。

答案

/**
 * 带超时、重试和指数退避的 fetch 请求工具
 *
 * @param {string | URL} url
 * @param {RequestInit & {
 *   retries?: number;
 *   retryDelay?: number;
 *   timeout?: number;
 *   retryOn?: (response?: Response, error?: unknown) => boolean;
 * }} options
 * @returns {Promise<Response>}
 */
async function requestWithRetry(url, options = {}) {
  const {
    retries = 3,
    retryDelay = 300,
    timeout = 5000,
    retryOn = (response, error) => {
      // 网络错误、超时,以及 408、429 和 5xx 通常适合重试
      return Boolean(
        error ||
          response?.status === 408 ||
          response?.status === 429 ||
          response?.status >= 500
      );
    },
    signal: externalSignal,
    ...fetchOptions
  } = options;

  let lastError;

  for (let attempt = 0; attempt <= retries; attempt++) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    // 外部 signal 取消时,同时取消当前请求
    const abortRequest = () => controller.abort();
    externalSignal?.addEventListener("abort", abortRequest, { once: true });

    try {
      const response = await fetch(url, {
        ...fetchOptions,
        signal: controller.signal
      });

      if (!retryOn(response)) {
        return response;
      }

      lastError = new Error(`Request failed with status ${response.status}`);
      lastError.response = response;
    } catch (error) {
      lastError = error;

      // 主动取消不应该继续重试
      if (externalSignal?.aborted) {
        throw error;
      }

      if (!retryOn(undefined, error)) {
        throw error;
      }
    } finally {
      clearTimeout(timeoutId);
      externalSignal?.removeEventListener("abort", abortRequest);
    }

    if (attempt === retries) break;

    // 指数退避 + 随机抖动,避免大量请求同时重试
    const exponentialDelay = retryDelay * 2 ** attempt;
    const jitter = Math.random() * retryDelay;
    await new Promise((resolve) => {
      setTimeout(resolve, exponentialDelay + jitter);
    });
  }

  throw lastError;
}

使用示例

const response = await requestWithRetry("/api/user", {
  method: "GET",
  retries: 3,
  retryDelay: 200,
  timeout: 3000
});

const data = await response.json();

实现要点

  • retries = 3 表示失败后最多重试 3 次,因此最多发起 4 次请求。
  • 使用指数退避:第 1 次等待约 retryDelay,第 2 次等待约 2 * retryDelay,以此类推。
  • 加入随机抖动,避免服务恢复时大量客户端同时发起重试请求。
  • 网络错误、超时、4084295xx 默认重试;400401403 等确定性的客户端错误通常不重试。
  • 通过 AbortController 实现单次请求超时,并支持调用方通过 signal 主动取消。
  • 非幂等请求(例如直接创建订单的 POST)不能盲目重试,应使用幂等键或服务端去重,避免重复创建数据。
  • 生产环境还可以读取 Retry-After 响应头,遵循服务端对 429503 的重试时间建议。