fetch 请求体不可重复读取,因其被封装为单次消费的 ReadableStream;重试时须每次新建 body,如用工厂函数生成 JSON、FormData 或 Blob,避免共享实例导致锁定错误。

fetch 请求体(Body)不能重复读取,这是由 ReadableStream 的单次消费特性决定的——一旦被 .json()、.text() 或 .arrayBuffer() 消费,流就关闭了。这个限制直接影响重试逻辑的设计,尤其在并发或多次尝试场景下极易引发错误。
为什么请求体不能复用
fetch 的 body(如 FormData、URLSearchParams 或 JSON.stringify() 后的字符串)在传入 fetch() 时会被封装为只读、不可重置的 ReadableStream。浏览器不会自动缓存或克隆它。这意味着:
- 若第一次 fetch 失败后直接重试,且仍传入同一个
body实例(如已构造好的FormData),第二次调用会抛出 TypeError: Failed to execute 'fetch': body stream is locked -
Request对象本身也继承该限制:创建一次new Request(url, { body })后,无法再次调用其.clone()来复用 body(.clone()仅对响应体有效,对请求体无效) - 常见误操作:把
body提前赋值为变量,在循环或递归重试中反复传入——这必然失败
对并发重试机制的实际影响
当多个重试请求并行发起(例如 Promise.all 或未加锁的递归重试),若共享同一份 body 实例,会出现竞争态:第一个 fetch 拿走流,其余全部报错。更隐蔽的问题是:
-
POST/PUT 类请求重试时静默丢数据:比如用
fetch(url, { method: 'POST', body: formData }),formData 在首次 fetch 中被消耗,后续重试传入空体或报错,后端收不到数据但可能返回 200,导致“看似成功实则失败” - 与幂等性设计冲突:即使服务端支持幂等 key,若重试时 body 已为空或结构异常,幂等校验失效,反而造成数据不一致
-
超时 + 重试组合下更易触发:例如设置
AbortSignal.timeout(5000),超时中断后流已锁定,紧接着的重试无法重建有效 body
安全复用请求体的可行方案
核心原则:每次重试都必须提供**全新构造的 body**。具体方式取决于 body 类型:
-
纯文本/JSON:把序列化逻辑封装为函数,而非提前计算好字符串
✅ 正确:body: () => JSON.stringify(data),重试时调用options.body()
❌ 错误:const bodyStr = JSON.stringify(data); fetch(..., { body: bodyStr }) -
FormData:每次重试前重新 new 一个实例,避免复用已有对象
✅ 正确:() => { const fd = new FormData(); fd.append('x', val); return fd; } -
Blob/File:需确保原始 Blob 可多次读取;若来自用户输入,可先
blob.arrayBuffer()缓存二进制,再每次新建new Blob([buffer]) -
使用 Request 构造器:可将整个 request 配置函数化,每次重试调用
makeRequest()返回新Request实例(注意:Request 的 body 仍不可 clone,但构造过程天然隔离)
结合重试函数的典型写法
以下是一个兼顾 body 安全与指数退避的最小可用模板:
async function fetchWithRetry(url, makeBody, options = {}, retries = 3) {
try {
const body = typeof makeBody === 'function' ? makeBody() : makeBody;
const res = await fetch(url, { ...options, body });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res;
} catch (err) {
if (retries > 0) {
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, 3 - retries)));
return fetchWithRetry(url, makeBody, options, retries - 1);
}
throw err;
}
}
调用时传入 body 工厂函数:
fetchWithRetry('/api/order', () => JSON.stringify({ item: 'A' }), { method: 'POST', headers: { 'Content-Type': 'application/json' } })

















