Node.js中如何正确取消正在进行的异步HTTP请求(fetch + AbortSignal)

文章导读
在 Node.js 后端服务中,异步 HTTP 请求是常见操作。但有时请求已经发出,却因为用户取消操作、超时、或者提前获取到所需数据等原因,需要停止正在进行的 fetch。如果不处理,这些请求会继续占用连接、内存和事件循环资源,甚至导致回调混乱。正确的取消机制不是靠强行结束进程,而是通过标准 API 让 fetch 自己优雅中止并释放资源。
📋 目录
  1. 为什么需要取消正在进行的 fetch 请求
  2. 取消机制原理:AbortController 与 AbortSignal
  3. 创建与传递信号:独立控制 vs 共享控制
  4. 超时取消实现:setTimeout 与 AbortSignal.timeout()
  5. 错误处理与资源清理:区分 AbortError 与网络错误
  6. 容易被忽略的检查点:已废弃信号不可复用与流式数据
  7. 总结性提醒
A A

为什么需要取消正在进行的 fetch 请求

在 Node.js 后端服务中,异步 HTTP 请求是常见操作。但有时请求已经发出,却因为用户取消操作、超时、或者提前获取到所需数据等原因,需要停止正在进行的 fetch。如果不处理,这些请求会继续占用连接、内存和事件循环资源,甚至导致回调混乱。正确的取消机制不是靠强行结束进程,而是通过标准 API 让 fetch 自己优雅中止并释放资源。

取消机制原理:AbortController 与 AbortSignal

在Node.js中取消fetch请求依赖于AbortController与AbortSignal的配合。AbortController提供一个abort()方法,调用后其signal对象的aborted属性变为true,同时触发'abort'事件。fetch API原生支持接收signal选项,当信号被中止时,fetch会抛出一个名为AbortError的异常,从而中断请求。这一机制不仅适用于浏览器,在Node.js 15及以上版本中也已内置支持,无需额外polyfill。

理解这个机制的关键是:AbortSignal 是连接控制器和请求的桥梁。你创建控制器,把它的 signal 传给 fetch,一旦 abort() 被调用,信号状态从待命变为已中止,fetch 内部检测到后立即抛出异常。这个异常不会破坏整个 Node.js 进程,只会让当前请求的 Promise 进入 rejected 状态,你可以在 .catch 中捕获并做出区别处理。

创建与传递信号:独立控制 vs 共享控制

使用AbortController非常简单:首先实例化一个控制器,然后将controller.signal作为fetch的选项传入。当需要取消请求时,调用controller.abort()即可。注意,同一个signal可以被多个fetch请求共享,但一旦abort()被调用,所有关联的请求都会同时被取消。如果希望每个请求独立控制,应为每个请求创建独立的AbortController实例。

这个选择直接决定了你取消请求的粒度。共享 signal 适合“一起取消”的场景,比如用户退出页面后取消所有正在进行的请求;而独立 signal 适合“每个请求有独立的超时时间”或“只取消其中某一个”的情况。在 Node.js 服务端,通常每个请求处理函数应该有自己的 AbortController,因为不同的 HTTP 请求之间不应该互相影响。如果一个 controller 被多个异步任务共用,一个超时可能导致其他任务意外中断,排查时很难定位。

超时取消实现:setTimeout 与 AbortSignal.timeout()

实现请求超时的一种常见做法是结合setTimeout与AbortController。创建一个AbortController,设定一个定时器,在超时后调用abort()。同时,将controller.signal传入fetch。为了避免内存泄漏,应当在请求完成或失败后清除定时器。更好的做法是使用AbortSignal.timeout()静态方法,它直接返回一个会在指定毫秒后自动中止的信号,简化了超时逻辑。

具体操作时,如果你使用 Node.js 16 及以上版本,推荐直接用 AbortSignal.timeout(5000) 作为 fetch 的 signal,省去手动管理定时器的麻烦。但需要注意:AbortSignal.timeout() 创建的信号是一次性的,每个 fetch 请求都需重新创建。如果你需要在请求结束后复用同一个 controller,就不要用这个静态方法。两种方式各有适用场景:手动管理 controller 适合“在请求过程中根据外部条件随时取消”,而 timeout 只适合固定超时。

Node.js中如何正确取消正在进行的异步HTTP请求(fetch + AbortSignal)

错误处理与资源清理:区分 AbortError 与网络错误

当 fetch 请求被取消时,reject 的 Error 对象 name 属性为 'AbortError'。在捕获异常时,应通过判断 error.name === 'AbortError' 来区分用户主动取消与网络错误,避免将取消视为异常上报。此外,取消请求后,如果请求已经发出,服务端可能仍在处理,但客户端不会再接收响应。在 Node.js 中,取消请求不会自动断开 TCP 连接,因此需要确保服务器端也有相应的超时机制避免资源浪费。

在代码层面,你应当在 .catch 或 try/catch 中加如下判断:

try {
  const response = await fetch(url, { signal });
  // 处理响应
} catch (err) {
  if (err.name === 'AbortError') {
    console.log('请求被用户取消或超时');
    // 不做异常上报,只记录请求被放弃的日志
  } else {
    console.error('网络错误或其他异常', err);
    // 进行错误处理或重试
  }
}

另外,如果 fetch 配合了 ReadableStream 进行流式读取,abort() 会立即关闭流,但已读取到的数据块依然存在。如果你的业务需要部分数据的回滚,需要自行设计补偿逻辑。

容易被忽略的检查点:已废弃信号不可复用与流式数据

使用AbortSignal时需注意,已废弃的signal(即已abort的信号)不能重复使用。如果将已中止的signal传给新的fetch请求,该请求会立即失败,抛出AbortError。因此,每次需要取消请求时,应创建新的AbortController。另外,对于流式响应(如ReadableStream),调用abort()会关闭流,但部分数据可能已被消费,需谨慎处理。

这意味着如果你打算给多个 fetch 使用同一个 controller,必须在 abort() 之前确保所有关联请求都已经完成。一个常见的坑是:在中间件或工具函数中复用全局 controller,结果一个请求超时后 abort,后续所有用到该 signal 的请求都莫名其妙地失败。排查时可以通过打印 signal.aborted 状态来确认。另外,在 Node.js 中,如果 request 已经被发出并且部分数据已经到达 Node.js 缓冲区,abort() 只是停止接收后续数据,并不会回滚已接收的缓存。如果你需要保证“开始即完成”的事务性,应该在业务逻辑层面处理,而不要依赖底层取消机制。

总结性提醒

说到底,AbortSignal 取消的是客户端一侧的等待行为,不能替代服务端的请求取消或限流。建议在服务端也设置 socket 超时(例如 http.Agent 的 timeout 选项)和业务超时中间件,作为双重保险。如果你的 Node.js 版本低于 15,fetch 需要先安装 node-fetch 或 undici,并且注意它们对 AbortSignal 的支持细节。每次升级版本后,最好在测试环境中验证取消功能是否仍然符合预期。