当前位置:首页 > 文章列表 > 文章 > 前端 > Web ReadableStream 怎样取消未读响应

Web ReadableStream 怎样取消未读响应

来源:17golang原创 2026-11-15 00:52:40 0浏览 收藏

Web ReadableStream 不再需要剩余响应数据时,应调用并等待 cancel()。尚未取得 reader 时可用 response.body.cancel();取得 reader 后流已被锁定,应改用 reader.cancel(),最后用 reader.releaseLock() 释放锁。releaseLock 只解除 reader 对流的占有,不能替代取消。

下面做一个只显示响应前 1024 字节的预览小项目。我会把“预览够了”和“用户点击停止”分别处理:前者取消剩余响应流,后者通过传给 fetch 的 AbortSignal 中止任务。这是设计取舍说明,代码与配图不代表已经完成实测。

Web API 文档地址:https://developer.mozilla.org/en-US/docs/Web/API

项目目标:保留前缀,取消剩余内容

预览功能只需要少量文本,却容易写成读取完整响应后再截取字符串。这个项目在应用侧只保留限定字节数,达到上限便表达“不再消费”的意图。预览上限约束的是返回给应用的字节数,不是精确的网络下载配额:浏览器可能已接收更多数据,一个 read 返回的数据块也可能比上限大。

项目包含 index.html、app.js 和用于示例的 sample.txt。页面只提供“开始预览”“停止”和一个文本输出区域,不需要框架。示例目标是理解单个响应流的所有权与清理,不处理 clone、tee 或管道分支。

环境准备:先认清三个对象的职责

使用支持 Fetch、ReadableStream、默认 reader、AbortController 和 TextDecoder 的浏览器,通过本地 HTTP 服务或已有开发服务器打开页面。使用同源 sample.txt 可减少跨域配置干扰;跨域接口还需满足服务端 CORS 设置。不要用 mode: no-cors 规避限制,因为不透明响应无法提供这里需要的可读响应内容。

操作使用场合实际职责
response.body.cancel(reason)流未被 reader 锁定放弃流中剩余数据并通知底层源
reader.cancel(reason)当前 reader 持有流由持有者发起取消
reader.releaseLock()清理 reader 所有权解除锁,不主动放弃未读块
controller.abort()fetch 使用了对应 signal中止相关 fetch 与响应体消费
响应流与reader锁定关系静态结构图
静态结构图:Response 的 body 是 ReadableStream,reader 与流之间存在锁定关系;取消入口取决于当前持有者,releaseLock 只处理占有关系。

取消会丢弃排队的未读数据,之后不能依靠重新获取 reader 恢复这些内容。若目的是交接给其他消费者继续读,应释放锁并明确交接,而不是先取消。释放锁时若还有未完成的 read,其 Promise 会因 TypeError 拒绝;不要靠释放锁来结束正在等待数据的业务。

核心代码:实现受限字节预览

把下面函数放入 app.js。它先处理 HTTP 错误,再取得 reader;循环中只保存所需前缀,正常到达末尾时不再额外取消,达到上限时则等待 reader.cancel 完成。

async function previewBytes(url, { signal, maxBytes = 1024 } = {}) {
  // 限制本示例的预览大小,防止误传参数造成大量内存分配
  if (!Number.isInteger(maxBytes) || maxBytes  65536) {
    throw new RangeError("maxBytes must be 1..65536");
  }
  const response = await fetch(url, { signal });
  if (!response.ok) {
    // 尚未获取 reader,此处可以直接取消未锁定的响应流
    try { await response.body?.cancel("http-status"); } catch {}
    throw new Error("HTTP " + response.status);
  }
  if (!response.body) throw new Error("response body unavailable");

  const reader = response.body.getReader();
  const parts = [];
  let total = 0;
  let ended = false;
  let cancelRequested = false;
  try {
    while (total  0) {
        // 复制需要的前缀,不把整个网络块长期保存在预览结果中
        parts.push(value.slice(0, take));
        total += take;
      }
    }
    if (!ended) {
      cancelRequested = true;
      await reader.cancel("preview-limit");
    }
  } catch (error) {
    // 读取异常时尝试清理;清理失败不覆盖原始错误
    if (!ended && !cancelRequested) {
      try { await reader.cancel(error); } catch {}
    }
    throw error;
  } finally {
    // 释放占有关系本身不代表取消,取消已在上面的分支处理
    reader.releaseLock();
  }

  const bytes = new Uint8Array(total);
  let offset = 0;
  for (const part of parts) {
    bytes.set(part, offset);
    offset += part.byteLength;
  }
  return bytes;
}

函数最多返回 maxBytes 个字节,参数限制 65536 是本示例自己的应用限制,不是浏览器 API 的上限。使用 slice 复制片段,是为了避免小预览长期引用整个大数据块的底层缓冲区。

cancel 的 Promise 也可能拒绝。正常预览分支会把这种失败传给调用方;异常清理分支只忽略二次清理错误,保留最初的读取异常。finally 始终释放 reader 锁。不要将所有错误都吞掉后显示“预览成功”。

本地接入:增加开始和停止操作

建立以下页面,再把按钮代码接在 app.js 的函数后面,放置一个 UTF-8 文本文件 sample.txt。通过项目自己的 HTTP 开发服务访问 index.html;本文不需要为了说明代码而部署线上服务。




响应前缀预览


const start = document.querySelector("#start");
const stop = document.querySelector("#stop");
const output = document.querySelector("#output");
let active = null;

// 串行启动,停止按钮通过同一个 signal 中止当前 fetch
start.onclick = async () => {
  if (active) return;
  const controller = new AbortController();
  active = controller;
  start.disabled = true;
  stop.disabled = false;
  output.textContent = "读取中";
  try {
    const bytes = await previewBytes("./sample.txt", {
      signal: controller.signal,
      maxBytes: 1024
    });
    // 仅用于 UTF-8 文本预览;字节截断可能使末尾字符不完整
    output.textContent = new TextDecoder().decode(bytes);
  } catch (error) {
    output.textContent = controller.signal.aborted
      ? "已停止"
      : String(error);
  } finally {
    active = null;
    start.disabled = false;
    stop.disabled = true;
  }
};
stop.onclick = () => active?.abort();

这里使用同一 controller 的 signal 连接请求和停止操作。每次新任务创建新的 AbortController;被中止的 signal 不能重置成未中止状态。按钮在当前任务结束前禁止重复开始,避免旧请求回写新任务结果。

预览任务的停止信号与返回数据静态关系说明图
静态说明图:停止操作属于 AbortController 与 signal 分组,前缀数据属于预览结果分组;信号关联 fetch,字节上限约束应用保存的预览结果。

TextDecoder 在这里仅负责 UTF-8 预览。1024 字节可能截在多字节字符中间,末尾出现替换字符不一定是源文件损坏。若要提供严格的字符或行边界预览,需要另外设计增量解码和文本截断规则,不能把字节上限解释成字符数。

部署集成时保留的边界与结果判断

集成到已有页面时,让任务层管理 AbortController,让读取函数负责 reader 的生命周期。取消流表示不再需要数据,不能承诺服务器立即停止计算、回滚写入或恰好少传某个字节数。对于已经完成的服务端动作,应通过业务协议处理,而不是把浏览器取消当作事务回滚。

可自行观察的场景应用应呈现的结果
文件小于预览上限读取到 done,显示全部已读内容
文件超过预览上限最多显示限定字节数对应的预览,取消剩余消费
任务尚未完成时点击停止中止当前任务,显示已停止
HTTP 非成功状态放弃响应体并显示 HTTP 错误
断网或读取失败保留错误,不伪装成正常到达末尾

这是给读者的运行判断清单,并非本文已经得到的测试报告。小文件可能在点击停止前已完成;需要观察停止状态时,应使用自己可控的慢响应,而不是假定每个静态文件都会持续下载。

为什么 response.body.cancel 报 TypeError?

常见原因是流已被 getReader 锁定。应让持有 reader 的代码调用 reader.cancel;不要绕过持有者抢夺同一响应体。

只写 releaseLock 后为什么还能继续读取?

它保留了流里的未读块,其他 reader 可以接手。这正是解除锁与取消数据消费的区别。

fetch 已经返回 Response,abort 还有用吗?

有可能仍有用:响应头已经返回不代表响应体已经消费完。对应 signal 的中止仍可影响后续响应体读取;若任务早已完成,就没有未完成的消费可中止。

参考:WHATWG Streams 标准、MDN releaseLock、MDN abort。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Linux getrandom 设置 GRND_NONBLOCK 后为什么返回 EAGAINLinux getrandom 设置 GRND_NONBLOCK 后为什么返回 EAGAIN
上一篇
Linux getrandom 设置 GRND_NONBLOCK 后为什么返回 EAGAIN
Web AbortController.abort 怎样保留取消原因
下一篇
Web AbortController.abort 怎样保留取消原因
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    1029次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    1030次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1173次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1134次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    976次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码