Node.js 中 AbortController 如何实现请求取消和超时控制?
简化版
AbortController 提供统一取消信号,Node.js 的 fetch、部分 fs、stream 和第三方库可以接收 signal,在超时、客户端断开或上游失败时取消任务。它能减少无效计算和资源占用,但前提是下游 API 支持 signal。
详细版
常见写法是创建 controller,把 signal 传给异步 API,超时后调用 abort()。
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 3000)
try {
const res = await fetch('https://api.example.com', {
signal: controller.signal
})
} finally {
clearTimeout(timer)
}
在服务端接口中,请求取消不是只给用户返回错误,还要尽量取消正在进行的下游请求、数据库查询或文件处理,避免客户端已经走了,服务器还在空转。
完整版教学
一、取消是服务端资源治理问题
用户关闭页面、网关超时、上游服务失败时,后端继续执行已经没有意义的任务。 如果这些任务还在调用数据库、下载文件或压缩图片,就会浪费 CPU、连接和内存。 取消机制就是把“这件事不用做了”的信号传下去。
客户端断开
-> Node 接口仍在执行
-> 下游 fetch 还在等待
-> 数据库连接被占用
假设每个无效请求占用 2 秒数据库连接,100 个断开请求就可能浪费 200 秒连接时间。 取消能让系统在异常流量下更稳。
二、AbortController 分成控制器和信号
AbortController 是控制端,signal 是传递给任务的只读信号。
调用 controller.abort() 后,signal 会变成已取消状态,并触发 abort 事件。
支持 signal 的 API 会据此终止工作。
const controller = new AbortController()
const { signal } = controller
signal.addEventListener('abort', () => {
console.log('cancelled')
})
controller.abort()
| 对象 | 角色 | 使用者 |
|---|---|---|
AbortController | 发起取消 | 调用方 |
signal | 传递取消状态 | 被调用 API |
abort() | 触发取消 | 超时或断开逻辑 |
AbortError | 取消错误 | 错误处理 |
这个模型比自定义布尔变量更通用。 多个 API 都能听同一个 signal,形成取消链路。
三、超时控制是最常见用法
网络请求必须有超时。
没有超时的请求可能一直挂着,拖垮连接池。
AbortController 可以和定时器组合,超时后取消 fetch。
async function fetchWithTimeout(url: string, ms: number) {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), ms)
try {
return await fetch(url, { signal: controller.signal })
} finally {
clearTimeout(timer)
}
}
如果接口 P95 是 200ms,业务可接受 1 秒,超时设 3000ms 可能太松。 超时时间应基于下游指标和用户体验,不是随手写一个 30 秒。
四、客户端断开也应该向下游传播
HTTP 服务可以监听请求或响应关闭事件。 当客户端断开时,取消下游任务。 这对下载、上传、AI 推理、报表导出等长任务尤其重要。
app.get('/proxy', async (req, res) => {
const controller = new AbortController()
req.on('close', () => controller.abort())
const upstream = await fetch(targetUrl, {
signal: controller.signal
})
upstream.body?.pipeTo(/* ... */)
})
浏览器取消请求
-> Node 收到 close
-> abort 下游 fetch
-> 释放 socket 和内存
如果不传播取消,代理层就会变成“替已经离开的用户继续干活”的冤种。
五、取消不等于所有工作都会立刻停止
只有支持 signal 的 API 才能被标准取消。
如果第三方库不支持 signal,调用 abort() 只能改变信号状态,不能强行杀掉内部工作。
这时要查库文档,或使用库提供的取消接口。
支持 signal:
fetch / 部分 fs API / 部分 stream API / 现代客户端库
不支持 signal:
需要库自身取消方法,或只能忽略结果
忽略结果和取消执行不是一回事。 忽略结果只是回调不处理,资源可能仍在被占用。
六、错误处理要区分取消和真实失败
取消通常会抛出 AbortError 或特定错误。
日志里应把用户取消、超时取消和真实 500 错误区分开。
否则监控会把正常取消当成系统错误。
try {
await fetchWithTimeout(url, 1000)
} catch (err: any) {
if (err.name === 'AbortError') {
console.warn('request aborted')
} else {
throw err
}
}
记忆钩子:AbortController 不是“让 Promise 不回调”,而是把取消信号沿链路传下去。
面试回答要覆盖超时、客户端断开、API 支持度和错误分类。 这比只写一个定时器更完整。
七、常见误区与追问
- 误区:Promise 本身可以被取消。 原生 Promise 没有取消语义,取消通常依赖 API 支持
signal。 - 误区:调用 abort 后所有任务都会立即停止。 只有支持 signal 或取消接口的任务才会真正停止。
- 误区:取消错误都应该按系统异常报警。 客户端断开和业务超时取消应分类记录,避免污染错误率。
- 追问:如何实现请求超时? 用
AbortController配合setTimeout,超时后调用abort(),最后清理定时器。 - 追问:客户端断开怎么传播? 监听
req.close或响应关闭事件,触发下游 controller abort。 - 追问:第三方库不支持 signal 怎么办? 查找库的取消 API;如果没有,只能避免处理结果并控制并发资源。
八、加强记忆
取消这题按“信号传播”记。Controller 发取消,signal 传给 fetch 或其他异步 API;超时和客户端断开都可以触发 abort;但取消是否真正生效取决于 API 支持。日志里要区分取消和真实错误。