Fetch ReadableStream cancel 怎样中止底层连接
来源:17golang原创
时间:2026-11-14 07:00:03 193浏览 收藏
我在把接口改成“边接收边展示”后,最先遇到的不是读取,而是用户切换页面时请求还在继续。这里要先把两个动作分开:ReadableStream.cancel() 表示消费者不再需要这条流;AbortController.abort() 才是给 Fetch 请求发送请求级中止信号。前者会把取消理由交给底层流源,但不应被当成所有浏览器都立即关闭底层 TCP 连接的保证。
官方文档:https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/cancel
如果目标是“停止解析已经拿到的响应”,用 reader 的取消方法;如果目标是“让整个 Fetch 请求和响应体消费一起结束”,从一开始就给 fetch 绑定 AbortController,并在取消时调用 abort()。
cancel() 和 abort() 不是同一个取消按钮
cancel() 解决的是流消费者的兴趣变化:已经不需要队列里剩下的数据,可以丢弃它们,并等待一个 Promise 表示取消动作完成。abort() 的范围更大,能够中止 Fetch 请求、响应体消费和相关异步操作;Fetch 被中止时,读取通常会以 AbortError 结束。
所以“中止底层连接”最好理解成“向 Fetch/流链路发出中止请求”,而不是在业务代码里直接操纵 TCP。浏览器是否已经建立连接、连接是否复用、服务器是否已经发送完数据,都不由 cancel() 的返回值单独告诉你。

先给 fetch 绑定请求级取消信号
如果页面有“停止生成”“切换会话”或组件卸载动作,推荐把控制器放在这次请求的生命周期里。下面的示例只展示取消职责,不依赖某个具体后端地址:
async function requestStream(url, onChunk, signal) {
// 把同一个信号交给 fetch,让请求和响应体共享取消入口。
const response = await fetch(url, { signal });
if (!response.ok) {
// HTTP 404/500 不会自动让 fetch reject,先显式检查状态。
throw new Error(`HTTP ${response.status}`);
}
if (!response.body) {
throw new Error("Response.body 不可用");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
// read() 在有数据、结束或异常时分别返回对应状态。
const { value, done } = await reader.read();
if (done) break;
onChunk(decoder.decode(value, { stream: true }));
}
} finally {
// 无论正常结束还是异常退出,都归还流的 reader 锁。
reader.releaseLock();
}
}
const controller = new AbortController();
const task = requestStream("/api/stream", renderChunk, controller.signal);
// 用户点击停止时,中止请求以及正在进行的响应体消费。
stopButton.addEventListener("click", () => controller.abort());
try {
await task;
} catch (error) {
// 用户主动取消是预期分支,其他错误仍需交给错误处理流程。
if (error.name !== "AbortError") throw error;
}
这里的关键是控制器要和这一次请求一一对应,不能把一个已经 abort() 过的信号长期复用到下一次 Fetch。控制器取消后,后续使用同一个 signal 的请求会立刻处于已取消状态。
拿到 reader 后,应该取消谁
调用 getReader() 后,Response.body 处于 locked 状态。此时不要再对同一个流直接调用 response.body.cancel(),因为流已经被 reader 锁住;应调用 reader.cancel(reason)。它表达的是“这个 reader 不再消费”,并把原因传给底层流源。
如果还没有取得 reader,且只是要放弃一个可读流,才可以使用 response.body.cancel()。两条路径的共同点是:取消是异步的,应等待返回的 Promise;取消后仍要在合适的边界释放锁。

async function readUntilMatch(response, target, controller) {
if (!response.body) throw new Error("响应没有可读 body");
const reader = response.body.getReader();
const decoder = new TextDecoder();
let text = "";
try {
while (true) {
// 只要找到目标文本,就不再等待后续 chunk。
const { value, done } = await reader.read();
if (done) return { found: false, text };
text += decoder.decode(value, { stream: true });
if (text.includes(target)) {
// reader 已经锁住流,使用 reader.cancel() 而不是 body.cancel()。
await reader.cancel("已找到目标,不再消费剩余数据");
controller.abort("已找到目标,停止对应 Fetch");
return { found: true, text };
}
}
} finally {
// cancel 只负责结束消费,锁的归还仍由当前 reader 负责。
reader.releaseLock();
}
}
这个例子同时调用 reader.cancel() 和 controller.abort(),是因为它既要结束当前流消费,也要让关联的 Fetch 尽快停止。实际项目也可以只保留控制器作为统一入口,但要保证所有并行读取都使用同一个 signal。
正常结束、主动取消和异常要分开
流读取最容易留下误判的地方,是把所有退出都显示成“请求失败”。可以按下面的口径处理:
done === true:服务器正常结束响应体,属于自然完成,不需要再调用取消。AbortError:通常是用户点击停止、组件销毁或超时控制器触发的预期取消,可单独记录为 cancelled。- 其他异常:可能是网络错误、解码错误或业务处理错误,应该保留错误信息并进入失败分支。
reader.cancel()失败:仍要执行releaseLock()的清理路径,但不要把它包装成已成功中止网络请求。
这套区分比单看一个“停止按钮是否变灰”更可靠:它告诉调用方是自然 EOF、明确取消,还是意外失败,也避免把用户主动停止误报成系统故障。
底层连接何时算真正结束
ReadableStream.cancel() 的规范语义是通知底层 source 消费者已经失去兴趣;底层 source 如何释放资源由它自己实现。对 Fetch 来说,AbortController.abort() 是更明确的请求级入口,但应用层仍不应根据一次 Promise resolve 就推断服务器端业务已经回滚或连接已经物理断开。
工程上可以把目标拆成三个层次:第一,UI 不再等待和渲染新的 chunk;第二,浏览器侧的 Fetch 和响应体消费收到 abort 信号;第三,服务端如果需要停止生成,还要通过协议或请求上下文识别断开并清理自己的任务。前两层由浏览器 API 配合完成,第三层需要后端单独设计。
最后检查这几个边界:控制器是否绑定了本次请求;拿到 reader 后是否使用了 reader.cancel;退出路径是否 releaseLock;是否把 AbortError 与真正异常分开。这样既能停止不需要的流,也不会把 cancel() 误当成一个直接关闭所有底层连接的万能开关。
常见问题
调用 reader.cancel() 后还要调用 releaseLock() 吗?
建议仍在 finally 中显式释放锁。取消和释放锁是两个不同责任:前者结束流消费,后者让流可以被后续代码重新接管。
只用 AbortController.abort(),还需要 reader.cancel() 吗?
如果控制器确实绑定了这次 Fetch,abort 通常足以结束请求和响应体消费;但 reader 仍然是当前代码持有的锁,清理代码依旧要释放它。需要在命中目标后立即放弃流时,显式调用 reader.cancel() 可以把意图写得更清楚。
ReadableStream.cancel() 会马上关闭服务器连接吗?
不能作这种绝对保证。它表达了消费者不再需要数据;若要求请求级中止,应使用绑定到 fetch 的 AbortController,同时让服务端处理客户端取消后的清理。
-
130 收藏
-
414 收藏
-
163 收藏
-
137 收藏
-
160 收藏
-
208 收藏
-
402 收藏
-
308 收藏
-
282 收藏
-
255 收藏
-
404 收藏
-
180 收藏
-
490 收藏
-
411 收藏
-
210 收藏
-
387 收藏
-
355 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习