当前位置:首页 > 文章列表 > 文章 > 前端 > 前端 Request 读取一次后怎样再次发送

前端 Request 读取一次后怎样再次发送

来源:17golang原创 2026-10-18 02:35:13 0浏览 收藏

前端里最容易让人困惑的一类错误是:先用 request.json() 记录请求内容,随后再把同一个 request 交给 fetch(),结果发送阶段提示请求体已经使用。原因不是 fetch 不能发送 Request,而是 Request 的 body 属于一次性可消费的流。要再次使用,关键动作是第一次读取或发送之前先调用 request.clone(),让不同消费者拿到不同的 Request 对象。

官方资料:https://developer.mozilla.org/en-US/docs/Web/API/Request/clone

要点速览
  • bodyUsed 反映当前 Request 的请求体是否已经被消费。
  • clone() 必须在原对象的 body 被读取或发送前调用,之后再 clone 会抛出 TypeError。
  • 一个分支可以用于日志或校验,另一个分支交给 fetch;如果要修改字段,则重新构造 Request。

先理解为什么同一个 Request 不能连续读取

Request.body 可以看成承载请求数据的 ReadableStream。调用 text()、json()、formData() 或把 Request 交给发送流程,都会让这条流进入已消费状态。bodyUsed 是一个很适合排查问题的只读布尔值:还没有消费时通常为 false,读取正文后会变成 true。

// 创建带 JSON 请求体的 Request,后面演示它的消费状态变化。
const request = new Request("/api/profile", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Lin" }),
});

console.log(request.bodyUsed); // 读取前为 false
const payload = await request.json(); // 消费请求体,并得到解析后的对象
console.log(payload.name);
console.log(request.bodyUsed); // 读取后为 true

因此,下面这种写法的思路本身就有冲突:先把 request.json() 的结果用于日志,再期待原对象仍然能被 fetch 读取。实际排查时,可以先观察 bodyUsed,但它只能帮助判断状态,不能把已经消费的 body 复原。

Request、ReadableStream、bodyUsed 与 json 和 fetch 消费关系的静态结构说明图
图1:Request 请求体消费关系说明图,展示 bodyUsed 与多个消费者的对象关系,不是浏览器截图。

在第一次消费前创建 clone

clone() 的作用是创建当前 Request 的副本。最稳妥的时机是拿到 Request 后立即分支,而不是等日志读取完成后再补救。一个常见的边界层可以把原始请求交给真正的发送逻辑,把副本交给日志或校验逻辑:

async function sendWithAudit(request) {
  // 先克隆,再让不同分支分别消费自己的请求体。
  const auditRequest = request.clone();

  // 日志分支读取副本,不会消耗真正要发送的 request。
  const auditBody = await auditRequest.text();
  console.debug("request body length:", auditBody.length);

  // 原始 Request 仍由 fetch 消费,适合继续保留原始 headers 和 body。
  return fetch(request);
}

这里的重点不是“复制后就能无限复制”,而是每一次 clone 都应该发生在对应对象尚未消费时。若日志、签名和发送需要三份独立读取,可以在最初状态一次创建足够的分支;不要先消费一份,再从已消费对象继续派生。

读取一份、发送一份的完整写法

如果边界层既要检查 JSON,又要把同样的请求继续发到后端,可以把副本交给解析逻辑:

async function inspectAndForward(input, init) {
  const request = new Request(input, init);
  // 必须在 json() 和 fetch() 之前 clone,避免两个消费者争用同一条流。
  const inspectRequest = request.clone();

  const body = await inspectRequest.json();
  if (!body.traceId) {
    throw new Error("缺少 traceId"); // 校验失败时不继续转发
  }

  // request 本身还未被消费,所以可以交给 fetch。
  return fetch(request);
}

inspectAndForward("/api/order", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ traceId: "t-100", amount: 18 }),
});

如果输入本来就是一个 Request,也可以把函数参数直接定义为 request,并在函数入口 clone。对于 Service Worker 一类需要读取并转发请求的场景,这种分支方式尤其常见。

原始 Request 分出审计副本和 fetch 副本的静态关系图,包含 clone、json 和发送分支
图2:读取与发送各自使用独立 Request 分支的关系说明图,不是运行结果或界面截图。

需要改字段时不要把 clone 当成编辑器

clone() 适合保留同一份请求的语义副本,但它不会帮你修改 JSON 字段、替换 URL 或自动添加业务头。如果需求是“读取旧请求,再带一个新字段发送”,通常应先读取数据,然后用 Request 构造器创建新的请求:

async function addTraceId(request) {
  // 读取 clone,保留原 request 供其他调用方决定是否发送。
  const source = await request.clone().json();
  const nextBody = JSON.stringify({ ...source, traceId: crypto.randomUUID() });

  // 新 Request 明确指定可修改的 body 和 headers,避免误以为 clone 会改字段。
  return new Request(request, {
    body: nextBody,
    headers: { "Content-Type": "application/json" },
  });
}

构造器接收一个 Request 作为输入时,可以继承它的一部分请求属性,再通过初始化参数覆盖需要改变的内容。新对象的 body 仍然只能消费一次,所以“重新构造”只是建立新请求,不等于取消流的一次性特征。

流式大请求要留意 clone 的内存代价

MDN 对 Request clone 的提醒很重要:底层行为类似 ReadableStream.tee,两个分支的背压速度取较快的一侧,较慢分支未读取的数据会在内部排队。如果请求体很大,或者一个分支读取很慢,clone 可能让未消费数据持续占用内存。

因此,小型 JSON 审计适合 clone;上传大文件、长连接流或两个消费者速度差距明显时,要重新评估设计。可以把校验前置到原始数据生成处、只记录元数据,或者在业务层保存可重复构造的字符串和二进制源,而不是把一个大型流复制给多个慢消费者。

常见问题

已经调用过 request.json(),还能再 clone 吗?

通常不能。Request 的 body 已经使用后,调用 clone() 会抛出 TypeError。应把 clone 提前到第一次读取之前,或者从仍然保留的原始数据重新构造 Request。

clone 后可以让两个 fetch 同时发送吗?

可以把原 Request 和副本分别交给不同的发送流程,但仍需考虑请求是否幂等、服务端是否会执行两次业务,以及大 body 在两个分支上的缓冲成本。clone 解决的是 body 的消费分支,不是重复提交的业务风险。

只想修改 URL 或请求头,必须先读取 body 吗?

不一定。若不需要检查 body,可以直接用新的 Request(request, { ... }) 构造副本并覆盖对应字段;只有要解析或改变 JSON 内容时,才需要先读取一份可消费的 body。

归纳起来,Request 再次发送的判断顺序很简单:先确认谁会消费 body,再在第一次消费前创建 clone;需要改数据时重新构造新的 Request;遇到大流或慢分支,则优先减少重复消费。这样处理,日志、校验和真正的发送就不会意外争用同一个请求体。

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