Node.js:给 fetch 加个超时,`AbortSignal.timeout` 很省事

43 次浏览5 条回复

写脚本调接口时,最怕请求卡住半天不返回。Node 18+ 里 fetch 可以配 AbortSignal.timeout,小工具里够用了。

环境:Node.js 18+。保存成 fetch-timeout-demo.mjs

const url = 'https://example.com'

try {
  const res = await fetch(url, {
    signal: AbortSignal.timeout(2000),
  })

  console.log(res.status)
  console.log((await res.text()).slice(0, 80))
} catch (err) {
  if (err.name === 'TimeoutError' || err.name === 'AbortError') {
    console.log('request timeout')
  } else {
    throw err
  }
}

跑一下:

node fetch-timeout-demo.mjs

这个适合一次性脚本、健康检查之类的场景。要是项目里已经有统一 HTTP 客户端,还是放到那层处理比较好。

这个还有个小坑:如果后面想兼容更早一点的 Node,可能得自己建 AbortControllersetTimeout。但只跑 Node 18+ 的内部脚本,用这个确实清爽很多。

另外批量请求时超时时间别设太死,外部接口偶尔抖一下,不然日志里会全是 timeout。

补一个细节:AbortSignal.timeout 返回的是 signal,本身不能手动取消。如果同一个请求还要支持用户主动取消,可以用 AbortSignal.any([userSignal, AbortSignal.timeout(2000)]) 把两个信号合在一起。

环境还是 Node 18+,不过 AbortSignal.any 要 Node 20 左右才比较稳;如果脚本跑在旧运行时,就别硬用这个写法了。

还有一点我觉得也可以顺手加上:超时只管“等太久”,不代表 HTTP 状态一定正常。像 404、500 这种 fetch 默认不会 throw,脚本里如果后面依赖返回内容,最好单独判断一下 res.ok

if (!res.ok) {
  throw new Error(`HTTP ${res.status}`)
}

不然有时候请求没超时,但其实拿到的是错误页,后面解析 JSON 才炸,会绕一点。

如果返回体后面要当 JSON 解析,我一般也会先看一下 content-type,至少避免 HTML 错误页混进来:

const type = res.headers.get('content-type') || ''
if (!type.includes('application/json')) {
  throw new Error(`unexpected content-type: ${type}`)
}

const data = await res.json()

小脚本里不用封太重,但超时、res.ok、返回类型这三件事都兜一下,排错会少很多绕路。

还可以补个清理点:如果自己用 AbortController 做超时,记得请求结束后把 timer 清掉,不然脚本里批量请求多了会留一堆没必要的定时器。

const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 2000)

try {
  const res = await fetch(url, { signal: controller.signal })
  console.log(res.status)
} finally {
  clearTimeout(timer)
}

AbortSignal.timeout 就不用管这个细节了,这也是它省心的地方。