资讯详情

Fetch API实战指南:从基础语法到上传、超时与报错排查

📅 2026/9/16 5:30:07 | 华诺云谱 👁 阅读
Fetch API实战指南:从基础语法到上传、超时与报错排查
1. 为什么我还在认真写 Fetch API前阵子接手一个项目前端同事把一整个 upload 组件从 XMLHttpRequest 迁移到 Fetch结果真机调试时频繁报“上传失败:网络请求错误”排查了两天才发现不是 Fetch 的锅而是请求体格式和 springboot 的 multipart 解析器对不上。这件事让我意识到很多同学对于 Fetch 的认知还停留在“能发请求、能拿 JSON”的层面一旦遇到上传、超时、取消、并发、后端联调就一脸懵。你如果搜“Fetch API”会看到大量文章都在重复 MDN 上那几段示例代码fetch(url).then(res res.json())。但真实项目里一次网络请求从浏览器发出到 springboot 后端处理完毕返回中间涉及请求头、body 编码、CORS、multipart 边界、Tomcat 连接器、异常处理、错误映射这一整条链路任何一个环节理解不到位都会在生产环境给你上一课。这篇文章我想从实战角度把 Fetch 讲透包括基础语法、请求配置、流式响应、上传下载进度、超时取消、并发控制以及一次完整的请求到达 springboot 后发生了什么。如果你正在处理“真机调试上传失败”“代码包大小超过限制”“tunneling socket 报错”这类问题第六部分的排查表可以直接拿来用。2. 先把 Fetch 和 XHR 的关系理清楚2.1 Fetch 不是 XHR 的替代品而是更底层的抽象很多人以为 Fetch 就是“新版 AJAX”这句话对了一半。XHR 是浏览器提供的一个历史悠久的网络请求接口功能完整但设计陈旧回调嵌套让人头疼。Fetch 基于 Promise 设计API 更简洁还能配合 async/await 写出非常清爽的代码。但它底层并不是 XHR 的封装而是浏览器基于 Stream 和 Promise 重新实现的请求模型所以它支持流式读取响应体也支持在请求阶段就取消AbortController这些是 XHR 时代很难做好的事。在实际项目里我是这样选择新代码统一用 Fetch老代码除非要兼容 IE 或者依赖 XHR 的上传进度否则不维护两套。Fetch 的兼容性在 2020 年之后的浏览器里已经非常稳定小程序端大部分容器也支持但要注意小程序里的 fetch 能力和浏览器存在差异这一点后面专门说。2.2 第一个 fetch 请求不会踩坑的写法先看一个最基本的例子直连一个公开接口拿数据async function getUser(id) { const response await fetch(/api/user/${id}, { method: GET, headers: { Accept: application/json } }); if (!response.ok) { throw new Error(请求失败: ${response.status} ${response.statusText}); } return response.json(); }关键点就三个fetch 返回的 response 是一个 Response 对象你拿到的不是你请求回来的数据本身而是一个“响应包装器”必须调用 response.json() 或 response.text() 才能读出具体内容。Promise 只有在网络错误断网、DNS 解析失败、连接被拒时才会 rejectHTTP 状态码 404、500 都不会进入 catch需要你自己用 response.ok 判断。headers 里的 Accept 是告诉服务器你期望的响应格式这不是必须的但养成习惯对后端接口设计有正向引导作用。这三句话是 Fetch 最核心的基础认知后面的所有进阶用法都建立在这三个认知之上。3. 请求配置每个参数背后都对应一类线上事故3.1 请求体格式是“上传失败”的头号元凶很多同学在发送 POST / PUT 请求时直接写fetch(/api/submit, { method: POST, body: JSON.stringify({ name: 张三, age: 18 }), headers: { Content-Type: application/json } });把 JSON 字符串放进 body这一写法和后端 RequestBody 完美配合没问题。但如果你跳过了 Content-Type或者后端用的是 RequestParam 而不是 RequestBody那就会得到 400 或者一个“参数绑定失败”的报错。更麻烦的是上传文件时很多人随手设了 Content-Type: application/json结果后端 multipart 解析器直接拒绝。正确的表单提交写法有两种。第一种是 URLSearchParams适合普通表单字段const formData new URLSearchParams(); formData.append(username, 张三); formData.append(age, 18); fetch(/api/login, { method: POST, body: formData, headers: { Content-Type: application/x-www-form-urlencoded } });第二种是 FormData适合上传文件或者混合表单const formData new FormData(); formData.append(file, fileInput.files[0]); formData.append(description, 项目附件); fetch(/api/upload, { method: POST, body: formData // 注意不要手动设置 Content-Type浏览器会自动带 boundary });这里是一个很典型的实战坑FormData 请求千万不能自己指定 Content-Type。因为 multipart 格式的 body 里每段数据之间有一个随机的 boundary 分隔符这个分隔符由浏览器生成并拼接在 Content-Type 里。你一旦手动设置了 Content-Type: multipart/form-data就丢了 boundary后端解析器直接懵掉。你发现“上传失败:网络请求错误”背后的真实原因很多时候就是这一行多余的 headers。3.2 credentials 不设登录态会悄悄丢Fetch 默认的 credentials 取值是 same-origin意思是同源请求会携带 Cookie跨域请求默认不带。你在开发环境通过 vite 或者 webpack 代理转发请求一切都是同源自然没问题。一旦上线后前端在 a.com接口在 b.com且后端配置了 CORS如果你没有显式设置fetch(/api/data, { credentials: include });那么 Cookie 永远不会带上后端 session 丢失接口会返回 401 或者跳登录。这个坑在前后端分离项目里非常高频而且难排查你看着请求正常状态码也正常就是拿不到数据控制台也没有明显报错最后发现只是少了一个字段。建议把 credentials: include 作为公司内部项目的默认配置而不是踩坑之后再加。3.3 超时控制比你想的更必要Fetch 原生没有超时机制你请求一个挂在半路上的接口可能要等满几十分钟才会被浏览器强制断开这期间按钮一直 loading用户反复点击产生一串重复请求。要解决这个问题必须借助 AbortControllerfunction fetchWithTimeout(url, options {}, timeout 10000) { const controller new AbortController(); const timer setTimeout(() controller.abort(), timeout); return fetch(url, { ...options, signal: controller.signal }) .finally(() clearTimeout(timer)); }AbortController 的功能不只是超时它可以在任意时刻中断请求这对表单“取消上传”按钮、搜索防抖场景非常关键。当 fetch 被 abort 后Promise 会 reject 一个 AbortError你可以通过 error.name AbortError 来区分是超时中断还是网络错误try { const res await fetchWithTimeout(/api/slow, {}, 3000); } catch (err) { if (err.name AbortError) { console.log(请求超时已被取消); } else { console.log(网络错误或其他异常, err); } }3.4 并发请求别只会 Promise.all 一把梭页面初始化经常要并行请求用户信息、配置项、消息列表三个接口直接 Promise.all 最简单但有个隐藏风险三个请求里有一个挂了整个 Promise.all 立即 reject剩下两个哪怕已经成功返回结果也拿不到。更合理的做法是用 Promise.allSettledconst [userRes, configRes, msgRes] await Promise.allSettled([ fetch(/api/user), fetch(/api/config), fetch(/api/msg) ]); if (userRes.status fulfilled) { // 使用 userRes.value }如果接口之间有依赖关系比如必须先拿 userId 才能请求用户详情那就老老实实用 async/await 顺序执行。还有一类场景是并发数量控制一次要上传 100 个文件浏览器不可能让你同时建 100 个请求网络一拥塞全挂。这时需要自己写一个简单的并发池控制同时执行的请求数量我通常限制在 4 到 6 个既能跑满带宽又不会打爆本地连接数。4. 响应处理与一个请求到达后端的完整链路4.1 Response 对象的正确打开方式fetch 返回的 Response 对象里ok 表示状态码是否在 200-299 区间status 和 statusText 是原始状态信息。响应体的读取方法有 json()、text()、blob()、arrayBuffer()但它们都是流式读取同一个 response 只能调用一次。你要是先 res.text() 再 res.json()第二次读取会直接报错因为流已经被消费完了。实际开发中我习惯写一个统一处理函数async function request(url, options {}) { const response await fetch(url, options); if (!response.ok) { // 尝试解析后端返回的错误信息 const errorText await response.text().catch(() ); throw new Error(${response.status} ${response.statusText} - ${errorText}); } const contentType response.headers.get(Content-Type) || ; if (contentType.includes(application/json)) { return response.json(); } return response.text(); }这里有个值得注意的点404 和 500 等错误响应也会带 body而且往往是后端精心设计的错误信息 JSON所以错误处理里也要尝试读取 body 内容别只丢一个“请求失败”就完事。我见过很多团队后端明明返回了“用户名已存在”这样的具体错误前端却只弹了一个“网络错误”用户完全不知道发生了什么。4.2 一次请求到 springboot 后的处理细节热搜词里有一条“一个网络请求到 springboot 后详细分析”这其实就是全栈联调最核心的认知。当你在浏览器控制台或代码里发起一个 fetch 请求后这个请求经历的过程大致是浏览器解析域名建立 TCP 连接如果是 HTTPS 还有 TLS 握手然后把 HTTP 报文发送出去请求到达后端服务器比如 Tomcat由 Connector 组件接收解析出 method、uri、headers、bodyTomcat 把请求包装成 HttpServletRequest交给 Servlet 容器接着进入 Spring MVC 的 DispatcherServletDispatcherServlet 根据 URL 找到对应的 HandlerMapping匹配到 Controller 里的方法如果方法参数有 RequestBodySpring 会调用 HttpMessageConverter 把请求体 JSON 字符串反序列化成 Java 对象如果是 MultipartFile则由 multipart 解析器处理方法执行完后返回结果再经 HttpMessageConverter 序列化成 JSON 响应给浏览器。后端配置会直接影响前端 fetch 的表现。举个例子Spring Boot 默认上传文件大小限制是 1MB你用 fetch 上传一个 3MB 的图片后端会抛出 MaxUploadSizeExceededException返回的 HTTP 状态码往往是 413 Payload Too Large但有的版本会包装成其他状态码。前端如果只判断 response.ok可能会看到 413 然后走错误分支却拿不到后端具体的英文报错用户只看到“网络请求错误”。如果你遇到“代码包大小超过限制”这种提示注意这不是后端配置问题而是前端打包后的产物超过平台限制。比如小程序主包或插件包体积超限微信开发者工具直接拒绝上传报“上传失败:网络请求错误”。这种场景和 fetch 完全无关问题出在构建产物太大需要做分包或静态资源 CDN 化。我处理过多次这种排查最后发现根本不是网络问题是开发者工具上传时对整体包体做了大小告警。下次看到这个报错先看是不是构建阶段出来的别一头扎进代码里查 fetch。4.3 下载文件与流式读取Fetch 下载文件也是一把好手。用浏览器原生方式触发下载可以这样const response await fetch(/api/file/export?typeexcel); if (!response.ok) throw new Error(导出失败); const blob await response.blob(); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download 报表.xlsx; a.click(); URL.revokeObjectURL(url);这种方式的优势是你可以随时拿到响应状态码如果后端返回 401 或者导出条件不满足你能捕获错误而不是像 window.open 那样直接跳一个无法控制的页面。对于大文件还可以流式读取const reader response.body.getReader(); const decoder new TextDecoder(); let result ; while (true) { const { done, value } await reader.read(); if (done) break; result decoder.decode(value, { stream: true }); }这在对接 SSEServer-Sent Events接口时尤其有用。后端一段一段推送数据前端通过 getReader 一段一段消费可以做出类似 ChatGPT 那种打字机效果。这种能力是 XHR 时代非常难实现的也是 Fetch 的一大进步。4.4 错误码与网络错误的分层处理前端在处理网络请求时最容易犯的错是把所有异常都一视同仁。我的建议是至少区分四类异常类型特征处理策略网络层错误fetch rejectTypeError: Failed to fetch提示检查网络支持重试HTTP 4xxresponse.ok 为 false4xx 状态码解析后端错误信息通常不重试HTTP 5xxresponse.ok 为 false5xx 状态码提示服务异常可自动重试一次业务错误状态码 200但业务 code 非 0按业务错误信息提示第一类“Failed to fetch”很常见但原因五花八门断网、DNS 解析失败、CORS 跨域被拦截、本地证书不受信任等。它的特征是 fetch 的 Promise 直接 rejectresponse 对象根本不存在。遇到这类错误我建议在 catch 里打印完整 error 对象把 name、message、cause 都打出来不然很容易把 CORS 问题和断网问题混为一谈。5. 进阶场景上传进度、取消与拦截器5.1 fetch 怎么做上传进度很多项目里表单提交需要实时显示上传进度条。XHR 有现成的 upload.onprogress 事件fetch 早期完全没有进度接口后来浏览器虽然支持了请求 body 的流式传输但前端要拿到上传进度还是得自己包一层。一个常见的方案是借助 XMLHttpRequest 实现进度但如果你坚持用 fetch也可以这样做async function uploadWithProgress(file, onProgress) { const formData new FormData(); formData.append(file, file); const response await fetch(/api/upload, { method: POST, body: formData }); // 如果你用的是支持 duplex 请求流的浏览器可以通过 ReadableStream 包装 body // 从而统计已经写入的字节数但兼容性一般 if (!response.ok) { throw new Error(上传失败); } onProgress(100); return response.json(); }说句实在话现阶段项目里如果核心需求就是上传带进度条我个人更推荐直接用 XHR没必要为了统一 API 而放弃成熟的能力。你可以封装一个内部函数对外暴露 Promise 风格接口内部用 XHR 实现这样调用方依然享受 async/await 的体验。等 fetch 的上传进度支持变得更成熟再切回去也不迟。5.2 请求拦截器与统一错误处理fetch 不像 axios 有现成的 interceptors 机制但你可以用一个高阶函数实现function withAuth(fetchFn) { return function (url, options {}) { const token localStorage.getItem(token); const headers { ...(options.headers || {}), Authorization: token ? Bearer ${token} : }; return fetchFn(url, { ...options, headers }); }; } const authedFetch withAuth(fetch);同理你可以继续包一层统一加 baseURL、统一请求日志、统一错误上报。把这些能力组合起来就能得到一个轻量级请求库。我甚至不建议在小型项目里引入 axiosfetch 配合业务层封装完全够用少一个依赖就少一分供应链风险多一层对底层原理的掌控。不过要提醒一句拦截器设计不要过度。我见过团队在 fetch 外层包了七八层每层都干点小活最后出问题根本不知道是哪层拦截的。保持结构扁平一层处理 URL 和 header一层处理鉴权一层处理错误上报够了。5.3 模拟网络请求的实践技巧在日常开发里模拟网络请求是调试的重要一环。最简单的方式是用 Mock Service WorkerMSW这类库拦截网络层它基于 Service Worker 实现可以在浏览器和 Node 环境同时使用你能在 UI 完全不做改动的情况下模拟接口返回。另一个方案是用 Charles 或 whistle 把线上接口的响应 map 到本地 JSON 文件适合排查线上问题和联调。如果你只是想快速验证 fetch 代码逻辑不依赖后端直接在 Chrome 控制台用 fetch 模拟请求// 模拟一个 POST 请求 fetch(https://httpbin.org/post, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ a: 1 }) }).then(res res.json()).then(console.log);httpbin.org 是一个非常方便的回显服务它会把你发送的任意请求参数原样返回用来验证请求格式、headers 是否正确再合适不过。如果你有内网环境也可以用 Postman 内置的 mock server不过对 fetch 调试来说没有 httpbin 直觉。6. 真实项目中的高频报错排查实录6.1 “真机调试 error: 上传失败:网络请求错误”的完整排查思路这个报错在微信小程序、H5 真机预览场景里出现频率极高。热搜词列了“真机调试 error: 上传失败:网络请求错误”我复盘一下排查路径。第一步看报错来源。微信开发者工具里的真机调试会先做“代码上传”这个阶段如果报“上传失败:网络请求错误”绝大多数是开发者工具和微信服务器之间的通信问题可能原因有本地网络不稳定、开发者工具版本过旧、代码包体积超限。先把工具更新到最新版然后在同一网络环境下用微信开发者工具普通预览试试如果普通预览没问题说明代码本身没问题是真机调试通道的问题。第二步看业务代码里的上传。如果上传是前端主动调 wx.uploadFile报错“上传失败:网络请求错误”多半是后端接口返回了非 2xx 状态或者返回了 HTML 而不是 JSON前端无法解析。通过开发者工具的 Network 面板看实际请求状态码对照 springboot 后端的访问日志定位。第三步检查域名合法性。小程序真机环境对请求域名有白名单校验非 https、未在小程序后台配置的域名会被拦截表现同样是网络请求错误。这一步很多老手也会漏掉因为开发模式下“不校验合法域名”开关一开所有问题都被掩盖上了真机就全暴露。还有一条代码包大小超过限制的坑我在 4.2 中提到过它是构建产物问题不是你代码里某个请求的问题。微信小程序主包上限通常是 2MB如果包含过大的图片、字体、第三方库上传时就会报“包体大小超过限制”提示语在部分版本被包装成“上传失败:网络请求错误”。解决方式是开分包或者用 CDN 托管静态资源。6.2 “tunneling socket 报错”与代理有关热搜词里出现“tunneling socket”这其实是 Node/Electron 环境里使用 fetch 或 axios 时命中代理设置后常见的一种错误。[object Object] 这个提示在现象里出现多次实际是错误对象被格式化成了字符串。tunneling socket 报错的本质是客户端尝试通过 HTTP 代理发送 CONNECT 请求建立到目标服务器的隧道但代理返回了非预期响应比如 407 需要认证、403 禁止访问或者代理和目标服务器之间的 TLS 握手失败。你在本地开发时如果配了全局代理这里不讨论具体代理类型只说机制Node 环境下的 fetch 会自动读取环境变量里的 HTTP_PROXY、HTTPS_PROXY然后走代理通道。排查方法很简单先看看本地环境变量里有没有 HTTP_PROXY、HTTPS_PROXY 设置有的话先临时清掉再跑一次。如果问题消失说明是代理隧道异常。在团队开发环境我一般建议把代理配置收口到配置文件里而不是散落在系统环境变量中避免不同成员环境差异导致“我这能跑你那不行”的经典僵局。6.3 常见报错速查表报错提示可能原因解决方向Failed to fetch网络断开/CORS 拦截/证书问题检查网络、跨域配置、证书上传失败:网络请求错误后端非 2xx、multipart 格式错误、包体超限、真机域名白名单按 6.1 的链路逐步排查tunneling socket 报错代理 CONNECT 隧道异常、代理需要认证检查代理配置、清理不必要环境变量Uncaught (in promise) SyntaxError: Unexpected token请求返回了 HTML但你用 json() 解析检查实际响应内容可能是 404 页面或网关错误页Body is unusable同一个 response 调用了多次 json()/text()缓存读取结果或只读取一次这张表是我多年排查网络请求类问题后沉淀的精简版覆盖了日常开发 80% 的 fetch 相关报错。每次看到一个新的“网络请求错误”第一反应不要是去翻库先自己走一遍这个表大概率能定位。6.4 调试网络请求的必备工具组合工欲善其事必先利其器。我平时调试 fetch 请求的工具组合是这样的浏览器 DevTools 的 Network 面板重点看 Request Headers、Payload、Response以及 Timing 里的每个阶段耗时后端日志springboot 项目可以在配置文件里开启 SQL 日志和请求日志配合 traceId 串联前端请求和处理过程whistle 或 Charles 做代理抓包和响应 mockNode 环境下可以用 undici 的 MockAgent或者直接起一个本地 mock server。如果请求跨域DevTools 里会看到 No ‘Access-Control-Allow-Origin’ header 的明确提示这时抓包看响应头就很关键。CORS 是浏览器行为curl 没有这个限制所以你会发现 curl 请求能通浏览器里却报错。这也是很多人困惑“接口明明能通为什么前端报错”的经典来源。记住这句话CORS 是浏览器帮你做的安全检查不是服务器拒绝了你是浏览器不让你拿到响应。7. 实际项目里我用 Fetch 时的一些个人习惯写到这里我想分享几个我在实战中形成的习惯不一定适合所有团队但参考价值还是有的。第一个习惯是给所有 fetch 请求统一封装一个 request 函数不在业务代码里直接调裸 fetch。原因很简单一旦后端要求加版本号、鉴权方式改变、需要统一埋点你只需要改一处而不是全局搜索替换。这个封装不要做得太重不要引入复杂的插件体系保持函数级几十行代码搞定。第二个习惯是区分并发和串行。有些接口之间没有任何依赖我会用 Promise.allSettled 并发请求有些接口有隐式依赖比如先拿到配置再决定请求哪个业务接口我会明确写成两步而不是在同步思维里强行搞并发。这看起来不是什么高深技巧但能避开很多“竞态条件”问题。第三个习惯是错误信息一定要带回显。不管用户看到的是“网络请求错误”还是“服务异常”都应该在后端错误信息里提取关键内容写进日志。我见过太多线上问题前端只上报了一个“request failed”后端日志又找不到对应记录两头对不上最后只能靠猜。如果你用我前面写的统一 request 函数天然就能把 status、url、errorText 一起捕获排查效率会高很多。第四个习惯是永远给 fetch 加超时。哪怕接口设计得再快网络波动、后端 GC、数据库慢查询都是不可控因素。一个没有超时的请求等于把你的页面生命周期交给了一个不确定的外部系统。8. 关于 Fetch 的未来与生态的一点看法Fetch 已经成为 WHATWG 标准的一部分浏览器对它的支持越来越完善。Node.js 从 18 版本开始也把 fetch 作为内置功能这意味着你可以在 Node 端和浏览器端用同一套 API 写网络请求对全栈开发者来说心智负担小了很多。虽然 Node 端 fetch 底层是基于 undici和浏览器实现略有差异但接口层面基本一致大部分代码可以直接复用。我判断未来两三年Fetch 会进一步挤压 axios 这类第三方库的生存空间尤其是在新项目里。axios 的核心价值是拦截器和统一错误处理但你仔细想想这些能力用高阶函数一百行以内就能复刻。对于追求少依赖、可控性强的团队fetch 是更优解。不过也要看清它的短板。上传进度支持不完整、SSE 需要手动解析、JSONP 完全不能做这些在特定场景依然要回落到底层 API 或者其他库。技术选型没有银弹理解 Fetch 的边界比无脑追捧或无脑唱衰都要务实。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。