Web ReadableStream 怎样取消未读响应
来源:17golang原创
时间:2026-11-15 00:52:40 491浏览 收藏
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 恢复这些内容。若目的是交接给其他消费者继续读,应释放锁并明确交接,而不是先取消。释放锁时若还有未完成的 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 不能重置成未中止状态。按钮在当前任务结束前禁止重复开始,避免旧请求回写新任务结果。

TextDecoder 在这里仅负责 UTF-8 预览。1024 字节可能截在多字节字符中间,末尾出现替换字符不一定是源文件损坏。若要提供严格的字符或行边界预览,需要另外设计增量解码和文本截断规则,不能把字节上限解释成字符数。
部署集成时保留的边界与结果判断
集成到已有页面时,让任务层管理 AbortController,让读取函数负责 reader 的生命周期。取消流表示不再需要数据,不能承诺服务器立即停止计算、回滚写入或恰好少传某个字节数。对于已经完成的服务端动作,应通过业务协议处理,而不是把浏览器取消当作事务回滚。
| 可自行观察的场景 | 应用应呈现的结果 |
|---|---|
| 文件小于预览上限 | 读取到 done,显示全部已读内容 |
| 文件超过预览上限 | 最多显示限定字节数对应的预览,取消剩余消费 |
| 任务尚未完成时点击停止 | 中止当前任务,显示已停止 |
| HTTP 非成功状态 | 放弃响应体并显示 HTTP 错误 |
| 断网或读取失败 | 保留错误,不伪装成正常到达末尾 |
这是给读者的运行判断清单,并非本文已经得到的测试报告。小文件可能在点击停止前已完成;需要观察停止状态时,应使用自己可控的慢响应,而不是假定每个静态文件都会持续下载。
为什么 response.body.cancel 报 TypeError?
常见原因是流已被 getReader 锁定。应让持有 reader 的代码调用 reader.cancel;不要绕过持有者抢夺同一响应体。
只写 releaseLock 后为什么还能继续读取?
它保留了流里的未读块,其他 reader 可以接手。这正是解除锁与取消数据消费的区别。
fetch 已经返回 Response,abort 还有用吗?
有可能仍有用:响应头已经返回不代表响应体已经消费完。对应 signal 的中止仍可影响后续响应体读取;若任务早已完成,就没有未完成的消费可中止。
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
348 收藏
-
文章 · 前端 | 23分钟前 | RTCDataChannel bufferedAmount bufferedAmountLowThreshold bufferedamountlow WebRTC背压471 收藏
-
253 收藏
-
172 收藏
-
321 收藏
-
文章 · 前端 | 31分钟前 | javascript · 前端开发 · 浏览器API · localStorage sessionStorage storage事件 Web Storage 同源文档200 收藏
-
424 收藏
-
文章 · 前端 | 37分钟前 | 前端 · javascript · Web API Fetch AbortController AbortSignal abort(reason) signal.reason318 收藏
-
208 收藏
-
402 收藏
-
308 收藏
-
282 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习