在 Node.js 里给 fetch 加超时和重试
可以,但 fetch 本身没有 timeout 参数,要配合 AbortController。Node 17.3 以后还可以直接用 AbortSignal.timeout(ms),一行就够:
await fetch(url, { signal: AbortSignal.timeout(5000) })
超时后会抛出 TimeoutError。
这是典型的「指数退避」重试。下面这个函数同时处理了超时和重试,只对网络错误、超时和 5xx 重试,4xx 直接返回,因为那通常是请求本身有问题,重试也没用。
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里。
最省事的办法是用 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 的回答可能有误,请核实重要信息。