在 Node.js 里给 fetch 加超时和重试

Node 18 自带的 fetch 能设置超时吗?

可以,但 fetch 本身没有 timeout 参数,要配合 AbortController。Node 17.3 以后还可以直接用 AbortSignal.timeout(ms),一行就够:

await fetch(url, { signal: AbortSignal.timeout(5000) })

超时后会抛出 TimeoutError。

那我还想要失败自动重试,最多 3 次,每次间隔变长。

这是典型的「指数退避」重试。下面这个函数同时处理了超时和重试,只对网络错误、超时和 5xx 重试,4xx 直接返回,因为那通常是请求本身有问题,重试也没用。

javascript
async function fetchWithRetry(url, options = {}, { retries = 3, timeout = 5000, baseDelay = 300 } = {}) {
  for (let attempt = 0; ; attempt++) {
    try {
      const res = await fetch(url, { ...options, signal: AbortSignal.timeout(timeout) });
      // 4xx 不重试,直接交给调用方处理
      if (res.status < 500 || attempt >= retries) return res;
    } catch (err) {
      if (attempt >= retries) throw err;
    }
    // 300ms, 600ms, 1200ms ... 再加一点随机抖动,避免大量客户端同时重试
    const delay = baseDelay * 2 ** attempt + Math.random() * 100;
    await new Promise((r) => setTimeout(r, delay));
  }
}

几点说明:

  • 抖动(jitter):如果很多客户端同时失败又同时重试,会把刚恢复的服务再打挂,随机延迟能把请求错开。
  • 幂等性:只对 GET、PUT 这类幂等请求放心重试;POST 重试可能导致重复下单,需要后端配合幂等键。
  • Retry-After:如果服务端返回 429 或 503 并带了 Retry-After 头,最好优先按它来等待。

AbortSignal 的详细行为可以看 MDN 的文档:https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static?utm_source=chat&utm_medium=assistant&utm_campaign=fetch-retry-explainer&ref=node18-abortsignal-timeout-static-method-reference

如果我想区分是超时还是网络断了呢?

看 err.name:

  • TimeoutError:AbortSignal.timeout 触发的超时。
  • AbortError:你自己调用了 controller.abort()。
  • TypeError:网络层失败,比如 DNS 解析不到、连接被拒,具体原因在 err.cause 里。
好的,那单元测试里怎么 mock 这个超时?

最省事的办法是用 Vitest 或 Jest 的假计时器,再把全局 fetch 替换成一个永远不返回、但会响应 signal 的函数。这样你可以用 vi.advanceTimersByTime(5000) 精确推进时间,测试不用真的等 5 秒。

注意 AbortSignal.timeout 在部分版本里不受假计时器控制,如果遇到这个问题,可以把超时实现改成 AbortController 加 setTimeout。

重试次数和间隔要不要做成可配置的?

上面的函数已经通过第三个参数支持了 retries、timeout 和 baseDelay。如果多个地方共用同一套配置,可以包一层:const api = (url, opts) => fetchWithRetry(url, opts, config.http),把配置集中到一个地方。

明白了,谢谢!

不客气。如果之后要在浏览器里复用同一套逻辑,这个函数基本不用改,现代浏览器都支持 AbortSignal.timeout。

Enter 发送,Shift + Enter 换行。AI 的回答可能有误,请核实重要信息。