前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库fetch 重试实现
JavaScript异步与 Promise

给 fetch 加重试时,怎么保证每次重试带的参数都和第一次一样?

重试函数自己保存好原始的 url 和 options,每次重试都用它们重新调用一次 fetch,不要复用上一次的请求对象。如果调用方传进来的是 Request 对象,每次发之前先 clone(),因为请求体只能被读一次。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#异步与 Promise#异步编程
先看核心答案读代码示例
理解线索

重试时参数会从哪里丢

  1. 请求体Request 发出后 bodyUsed 变成 true,再发一次直接 TypeError
  2. options拦截器改过 headers 的话,重试会带上被改过的版本
  3. 流式 bodyReadableStream 读完就没了,想重试要先把内容读成字符串或 Blob

options 里的 body 是字符串、FormData 或者 Blob 时,每次调用 fetch 都会重新生成请求体,可以放心重发。只读一次的限制针对的是 Request 对象和流。

核心回答

先记住这个答案

参数丢失通常出在两个地方。一是复用了已经发过的 Request 对象,它的请求体只能读一次,第二次发会直接抛 TypeError。二是重试时用的 options 被中间环节改过了,比如拦截器往 headers 里塞了东西。所以重试函数要自己保存一份原始的 url 和 options,每次重试都用它们重新调用 fetch;如果拿到的是 Request 对象,就保留原件不动,每次发 request.clone()。

  • 保存原始 url 和 options,每次重新调 fetch
  • Request 的请求体只能读一次,重发前要 clone()
  • fetch 遇到 500 不会拒绝,要自己看 res.ok
  • 主动取消和 4xx 不重试,POST 要有幂等保证

一个能用的重试函数

先把重试写对:该重试的重试,不该重试的立刻返回,间隔逐渐拉长。

保存参数,按指数退避重发JavaScript
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))

async function fetchWithRetry(url, options = {}, retries = 3) {
  for (let attempt = 0; ; attempt++) {
    try {
      const res = await fetch(url, options) // 每次都用同一份 url 和 options 重发
      // 成功、4xx、或者次数用完,都直接交给调用方
      if (res.ok || res.status < 500 || attempt >= retries) return res
    } catch (err) {
      // 用户主动取消不重试;网络错误在次数用完后才抛出
      if (err.name === 'AbortError' || attempt >= retries) throw err
    }
    await sleep(500 * 2 ** attempt) // 依次等 500、1000、2000 毫秒
  }
}

有两点容易漏。fetch 只有网络不通、跨域被拦、被取消时才会拒绝,服务端返回 500 时 Promise 是正常 resolve 的,所以要自己检查 res.ok 和 res.status。另外 4xx 通常是请求本身有问题,比如参数错、没权限,重试多少次结果都一样,应该直接返回。 间隔用指数增长,叫指数退避,意思是每失败一次,下次多等一倍。服务端已经过载时,所有客户端都立刻重试只会雪上加霜。

Request 对象为什么不能直接重发

请求体在底层是一个流,发请求时浏览器把它从头读到尾发出去,读完就没有了。Request 对象会记录这件事,发过一次之后 bodyUsed 变成 true。

复用和 clone() 的区别JavaScript
const req = new Request('/api/order', {
  method: 'POST',
  body: JSON.stringify({ sku: 'A1', count: 1 })
})

await fetch(req.clone()) // 发的是副本,原件的 body 没被读
await fetch(req.clone()) // 重试时再复制一份,照样能发

await fetch(req) // 原件被读掉了
await fetch(req) // TypeError:请求体已经用过
// req.clone() 也不行了,已经用过的 Request 不能再 clone

所以正确的顺序是先 clone() 再发,把原件一直留在手里当模板。如果等请求发完再去 clone(),已经来不及了。

流式请求体只能自己缓存
如果 body 本身是 ReadableStream,比如边读文件边上传,clone() 也只是把同一个流分成两份,内存里会攒下整份数据。这种请求要么在重试前先把内容读成 Blob 保存,要么干脆不支持自动重试,交给用户手动重传。

重试会不会重复下单

GET 请求重试没什么风险。POST 就要小心:第一次请求其实已经到了服务端并创建了订单,只是响应在路上丢了,前端以为失败又发了一次,结果就有了两个订单。

常见做法是前端在第一次发请求前生成一个唯一 ID,放进请求头,比如很多支付接口约定的 Idempotency-Key,重试时带同一个值。服务端看到重复的 key 就返回上次的结果,而不是再创建一次。这个 ID 必须在重试函数外面生成、和 options 一起保存,否则每次重试都是新 ID,就失去了意义。

回答前,多想一步

容易答错的地方

以为 fetch 遇到 500 会进 catch
只有网络层面失败才会拒绝。只在 catch 里重试的话,服务端返回 500 或者 503 时一次都不会重试。
在循环里改 options
有人在重试前往 options.headers 里追加新 token,直接改的是调用方传进来的对象,调用方下次再用这份 options 时也被污染了。要改就先复制一份,比如 { ...options, headers: new Headers(options.headers) },再在副本上改。
用户取消了还在重试
AbortError 说明是用户或组件主动放弃的,这时继续重试就违背了取消的本意。catch 里要先判断错误名,是取消就直接抛出去。
试着用自己的话回答

面试官还会怎么问?

服务端返回 429 要怎么处理?

429 表示请求太频繁,属于 4xx 里少数值得重试的。如果响应带了 Retry-After 头,就按它给的秒数等待,而不是用自己的退避间隔。503 也可能带这个头。

多个客户端同时失败,怎么避免它们同一时刻重试?

在退避间隔上加随机抖动,比如在计算出的等待时间上随机加减一部分。这样大家的重试时间被打散,不会在服务恢复的瞬间一起涌进来。

每次重试要单独设超时吗?

要。整体超时和单次超时是两回事:单次超时防止某一次请求挂住,整体超时限制加上重试一共最多等多久。单次超时可以用 AbortSignal.timeout(ms),再用 AbortSignal.any 和调用方传进来的取消信号合在一起。

从一道题,走向一组知识

把知识连起来

异步与 Promise

一个请求既要能被用户取消又要有超时,AbortSignal.any 怎么用?

重试函数通常要同时响应用户取消和单次超时,这两个信号用 AbortSignal.any 合成一个。

异步与 Promise

Promise.all 里一个请求挂了,怎么让其他请求的结果还能用?

并发发多个请求时,某个请求重试到底仍失败,要靠那篇的办法保住其他请求的结果。

参考资料

  • JavaScript:异步与 Promise参考资料

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 一个能用的重试函数
  3. Request 对象为什么不能直接重发
  4. 重试会不会重复下单
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑