fetch:超时、重试与 AbortController

fetch 是浏览器里发 HTTP 请求的标准方法。默认它不会因「等太久」自动失败,也不会帮你重试。

线上接口偶发超时、网关 503、限流 429,若页面一直转圈或疯狂连打,体验与服务都会受伤。下面把三件事拆开:超时怎么取消、什么错误值得重试、怎么和 AbortController 配合。

用 AbortController 做超时取消

一、先说一个具体麻烦

你写了:

const res = await fetch('/api/report')
const data = await res.json()

上面代码中,没有超时。服务端卡住时,Promise 可能一直 pending,按钮转个不停。用户重复点击,又发出第二、第三次请求。

需要两样能力:

(1)到点取消
(2)区分「可重试」与「不该重试」,避免把 400 校验错误也重打三遍

二、AbortController:取消的总闸

AbortController 提供一个 signal。把它传给 fetch,调用 abort() 即可取消。

const ac = new AbortController()
fetch('/api/report', { signal: ac.signal })
// 某处:
ac.abort()

上面代码中,取消后 fetch 会以 AbortError 一类错误拒绝(具体名称因环境略有差异)。同一 signal 也可传给多个关联请求,一并取消。

超时只是「到时间自动 abort」的一种用法。

三、最小超时封装

思路:(A)创建 controller(B)setTimeout 到点 abort(C)finally 里清定时器。

export async function fetchWithTimeout(url, options = {}, ms = 8000) {
  const ac = new AbortController()
  const timer = setTimeout(() => ac.abort(), ms)
  try {
    return await fetch(url, { ...options, signal: ac.signal })
  } finally {
    clearTimeout(timer)
  }
}

上面代码中,若调用方自己也传了 signal,生产代码还应把外部 abort 与超时 abort 合并(例如 AbortSignal.any,或手动监听转发)。入门先掌握「超时 = 定时 abort」。

用户离开页面或关闭弹窗时,同样应 abort,避免无用请求占着连接与回调。

四、什么错误才重试

不是所有失败都该重试。

通常更值得重试:

(1)网络断开、DNS 临时失败
(2)408 / 429 / 502 / 503 / 504
(3)你明确标成幂等的 GET / 某些可安全重放的写操作(需业务保证)

通常不要盲着重试:

(1)400 / 401 / 403 / 404 / 422(改请求或鉴权,不是再打一次)
(2)非幂等 POST 已可能成功但响应丢失——重试可能造成重复下单(需幂等键)

可重试错误才退避重试

五、退避重试的最小形状

「立刻再打」容易把服务打崩。常见做法是指数退避:1s、2s、4s……并加一点抖动。

async function fetchWithRetry(url, options, { retries = 2, baseMs = 400 } = {}) {
  let lastErr
  for (let i = 0; i <= retries; i++) {
    try {
      const res = await fetchWithTimeout(url, options, 8000)
      if (res.ok) return res
      if (![429, 502, 503, 504].includes(res.status) || i === retries) return res
    } catch (err) {
      lastErr = err
      if (i === retries) throw err
    }
    const wait = baseMs * 2 ** i + Math.random() * 100
    await new Promise((r) => setTimeout(r, wait))
  }
  throw lastErr
}

上面代码中,逻辑是示意:成功或不可重试状态码就返回;可重试则等待后再来。真实项目还应:

(1)尊重 Retry-After
(2)把超时 abort 与「主动取消」区分开——用户取消不应再重试
(3)对写操作加幂等键或禁止自动重试

六、和 UI 状态机对齐

请求层做好了,还要和界面约定:

(1)进行中:禁用提交或显示 loading
(2)取消:abort,并忽略随后的过期响应
(3)失败:按状态码给文案(401 去登录,429 提示稍后再试)

竞态常见坑:先发的慢请求后返回,覆盖了新数据。用递增的 request id,或每次新请求 abort 旧请求,可以避免。

七、常见误区

(1)只 await fetch,不看 res.ok
HTTP 404 不会进 catch,需要自己判断。

(2)所有方法统一重试三次
非幂等写操作很危险。

(3)超时时间设得极短又极多重试
等于自己制造雪崩。

(4)catch 到 AbortError 还弹「网络错误」
用户主动取消应静默处理。

(5)忘记清 timer
组件卸载后仍 abort,或 timer 泄漏。

八、小结

fetch 本身不管超时与重试。用 AbortController 做取消,用定时 abort 做超时,用状态码与幂等性决定是否退避重试。

三件事分开设计,再组合;混在一个「万能 request」里最容易变成黑盒。

(完)