当前位置:首页 > 文章列表 > 文章 > 前端 > Fetch ReadableStream cancel 怎样中止底层连接

Fetch ReadableStream cancel 怎样中止底层连接

来源:17golang原创 2026-11-14 07:00:03 0浏览 收藏

我在把接口改成“边接收边展示”后,最先遇到的不是读取,而是用户切换页面时请求还在继续。这里要先把两个动作分开: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 请求、Response.body、ReadableStream.cancel 与 AbortController.abort 的职责边界说明图
图1:Fetch 取消职责说明图,区分放弃流消费与中止请求,不是浏览器截图或运行证据。

先给 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;取消后仍要在合适的边界释放锁。

ReadableStream getReader 锁、reader.cancel、releaseLock 和 finally 清理边界结构图
图2:reader 锁与清理边界结构图,展示 cancel 后释放锁的责任关系,不是运行截图。
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,同时让服务端处理客户端取消后的清理。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
TypeScript NoInfer 怎样阻止默认值反向影响泛型推导TypeScript NoInfer 怎样阻止默认值反向影响泛型推导
上一篇
TypeScript NoInfer 怎样阻止默认值反向影响泛型推导
CSS field-sizing content 与 min-height 怎么配合
下一篇
CSS field-sizing content 与 min-height 怎么配合
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    1019次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    1021次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1155次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1122次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    963次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码