资讯动态

Failed to fetch与404排查:从浏览器到网关分层定位

发布时间:2026/9/17 18:25:47 来源:尧图企业网站定制
Failed to fetch 和 404 Not Found 这两个词几乎绑在一块出现是我在排查接口问题时见面次数最多的组合。前端控制台红着一行TypeError: Failed to fetch点开 Network 面板一看某个请求的状态码是 404或者反过来接口返回一段 JSON 写着{detail:not found}上层封装直接把这段响应转成一个 Failed 的异常抛到业务代码里。这两种情况看起来是同一个问题实际根因完全不在一个层面上。这篇内容我想把这条链路上的事情说透Failed to fetch 到底在什么时刻被抛出来、404 有几种完全不同的含义、从浏览器到网关该怎么一层层剥、以及本地开发、构建部署、调用第三方服务这三类场景下最常踩的坑长什么样。写给谁看写给刚接手一个前后端分离项目、第一次遇到刷新页面就白屏的人也写给已经被这类问题磨过几轮、想建立一套固定排查动作的人。文中所有步骤都是可以直接照着做的命令和配置我也尽量给到能复制的程度。1. 两个报错名词的边界先把 Failed to fetch 和 404 分清楚很多人第一次看到这两个词同时出现会下意识认为它们是因果关系——因为 404 了所以 Failed to fetch。这个理解在部分场景下成立但在更多场景下是错的而且一旦搞错方向排查时间会成倍增加。这一节先把两者的边界画清楚。1.1 fetch 什么时候才会抛 Failed to fetch浏览器原生的fetch()有一个很容易被误解的设计它对 HTTP 状态码不敏感。也就是说服务端返回 404、500、502fetch都认为这是一次成功的通信Promise 会正常 resolve你拿到的是一个response对象只是response.ok为false、response.status是 404。它不会因为这个 reject。只有当请求根本没走完网络这一层fetch才会 reject并抛出一个TypeError消息文本恰好就是Failed to fetch。会触发它的典型情况有这么几种域名解析不了、目标端口没有服务在监听、请求被 CORS 策略拦住预检失败或响应头缺Access-Control-Allow-Origin、HTTPS 页面里请求了 HTTP 资源被混合内容策略拦截、请求被AbortController主动取消、请求被 Service Worker 拦截后处理失败。有个细节值得单独拎出来CORS 被拦的请求在 Network 面板里的 Status 常常显示为(failed)或者CORS error而不是 404。如果你看到 Status 明确是 404那基本可以排除 CORS问题在服务端确实没有这个资源。反过来如果你看到的是 Failed to fetch 而 Network 里根本没有这条请求记录那大概率是请求在发出前就被拦了重点查跨域、混合内容和拦截器。那为什么那么多项目里404 会表现为 Failed to fetch因为中间隔了一层封装。axios、各类 SDK、自己写的request.ts很多都在拦截器里做了这样一件事if (!response.ok) throw new Error(Failed to fetch)或者干脆统一抛一个自定义错误。这时候业务代码看到的 Failed to fetch其实是封装层对 404 的转译真正的原始信息被吞掉了。这是我最想提醒的一点排查之前先确认你的项目里有没有做这种错误转译如果有第一步应该是把原始 status 和 url 打出来而不是继续追 Failed to fetch 这个字符串。1.2 404 不是一个错是五种错404 这个状态码只说明一件事服务端收到了请求但找不到你请求的那个资源。至于找不到发生在哪一层差别巨大。我在实际项目里把它归成五类每一类的排查入口都不一样。第一类是前端路由 404。单页应用用 history 模式地址栏里是/user/123这个路径在服务器上并不存在对应的文件用户直接访问或者按 F5 刷新服务器找不到文件就返回 404。这类问题的特征是站内点击跳转一切正常只有刷新和直达链接出问题。第二类是静态资源 404。打包后的 JS、CSS、图片路径对不上通常是publicPath、base配置和实际部署目录不一致导致的或者文件名带了内容哈希而 CDN 上还是旧版本。第三类是路径拼接 404。接口地址多一个斜杠、少一个斜杠、大小写不一致、版本前缀/v1漏掉都会让后端路由匹配不上。这类问题最隐蔽的地方在于它往往只在某个环境出现因为不同环境的环境变量写法不一样。第四类是代理与网关 404。请求发出去了但 Nginx 的location没匹配上、proxy_pass的斜杠写反了、rewrite 规则把路径改坏了流量根本没到后端应用或者到了但路径已经被改得认不出来。第五类是上游能力不存在。调用第三方接口时模型名写错、接口版本不存在、文档路径拼错对方直接返回一段 404 JSON。这类返回里通常带着很详细的提示比如明确告诉你某个模型不存在这种信息其实是排查的捷径别浪费。把这五类分清楚后面的排查动作才有意义。我见过太多人拿着一个刷新 404 的问题去翻后端日志翻了两小时也没结果因为请求压根没到后端。2. 排查顺序从浏览器到网关一层一层剥排查这类问题最忌讳的是想到哪查到哪。我自己的固定顺序是先在浏览器里把事实固定下来再用命令行把浏览器变量排除最后按链路顺序逐层验证。这个顺序不能颠倒原因在后面会说。2.1 开发者工具里先固定三样东西打开 DevTools 的 Network 面板勾上Preserve log然后复现一次问题。接下来只做三件事把三样东西抄下来暂时不做任何判断。第一样是完整的 Request URL。注意是完整的那种从协议头开始包括端口和查询参数。我习惯右键请求选Copy as cURL这样拿到的东西最完整也最不容易漏掉不可见字符。第二样是 Status 和 Type。Status 是 404 还是(failed)Type 是xhr、fetch、script、document还是img这两条信息加起来基本能定位到前面五类中的哪一类。比如 Type 是document且 Status 是 404八成是前端路由问题Type 是script且 404就是静态资源路径问题。第三样是 Response 的原始内容。只有 404 状态码还不够要看返回体是什么。如果是一整段 HTML说明是被某个 Web 服务器或 CDN 的默认错误页接住了如果是一段结构化 JSON 带detail或message字段说明请求已经到达应用层是应用自己返回的 404。这两种情况指向的修复位置完全不同。顺手看一眼 Response Headers 里有没有server、x-powered-by、cf-cache-status之类的字段能帮你判断这条 404 到底是谁生成的。这是个小技巧但信息量很大。2.2 用 curl 把浏览器变量排除掉浏览器里能出问题的地方太多了跨域、Cookie 策略、缓存、Service Worker、扩展插件。所以在动手改任何配置之前我强烈建议先用 curl 打一遍同样的地址。curl -i -X GET https://api.example.com/v1/users/list?page1 \ -H Authorization: Bearer token \ -H Accept: application/json-i的作用是把响应头一起打出来这比只看 body 有用得多。重点看三个地方状态行是 200 还是 404Content-Type是application/json还是text/html以及响应体开头是什么样的。如果 curl 返回 200浏览器返回 404那问题几乎必然在浏览器侧——跨域、代理、Service Worker 缓存、请求 URL 被某个拦截器改写了。如果 curl 也返回 404那就跟浏览器无关了方向转到服务端和网关。这一步能砍掉一半的无效排查。还有个小坑token 这类东西从浏览器复制出来时经常带着换行或者首尾空格尤其在 Windows 环境下。我一般会先echo一下确认长度和内容再往 curl 里塞。2.3 分层剥离法的顺序为什么不能反假设 curl 也返回 404接下来按前端 - 代理 - 应用的顺序逐层验证。这个顺序的理由是每一层的输入都是上一层的输出从最靠近你的一端开始验证可以最快地把问题圈在一个区间里。具体做法是先绕开代理直接打后端应用的真实地址。如果直连后端返回 200但经过代理返回 404那问题就在代理配置上跟应用代码没关系。如果直连后端也 404那就往应用内部看路由有没有注册、请求方法是不是POST打成了GET、路径参数有没有传空值、有没有前置的鉴权中间件提前返回。在容器环境里这一层还要多一步确认你连的是不是你以为的那个服务。容器里的localhost指向容器自身不是宿主机127.0.0.1同理。如果应用在容器里跑数据库或者另一个服务在宿主机上用localhost一定连不上。这时候要么用宿主机在容器网络里的地址要么把两个服务放进同一个 compose 网络里用服务名互相访问。这个问题我至少遇到过五次每次都有人花很久才想到。3. 五类高频 404 的成因与修法上一节讲的是排查方法这一节把五类问题的具体成因和修复方式摊开说。每一类我都配了典型现象和可复制的配置你可以直接对照自己项目的情况。3.1 静态资源与前端路由类刷新就白屏的元凶这类问题的典型现象是从首页点进去一切正常一刷新就是 404 或者白屏Network 里那条 404 请求的 Type 是document。根因是 history 模式的路由在客户端拦截了地址栏变化服务器并不知道/order/detail/88这种路径该返回什么文件。修复方式是在服务器层做兜底所有没匹配到真实文件的请求一律返回index.html剩下的交给前端路由处理。Nginx 下就是一行配置location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }try_files会依次尝试是否存在这个真实文件、是否存在这个目录、都不存在就返回index.html。这里有个容易搞混的点——如果你的项目部署在子路径下比如/admin/那index.html也要带上这个前缀root和try_files都得对应调整否则兜底会兜到错误的位置。另一个相关的是构建工具的base配置。Vite 里是basewebpack 里是publicPath。如果部署在子路径而这里还是默认的/打包出来的资源引用地址就会指向根目录结果就是 JS、CSS 全部 404。判断方法很简单看 Network 里那条 404 的 script 请求 URL路径上少的那一段就是这个配置该填的值。顺带说一句哈希路由#那种不会有这个问题因为#后面的内容不会发给服务器。有些团队为了省事直接换成哈希模式这确实能绕开但代价是 URL 不好看、分享出去带一长串符号而且不利于某些依赖路径的统计。能用服务端兜底解决我建议还是别换。3.2 路径拼接类多一个斜杠就全盘皆输路径拼接错误的隐蔽性在于它看起来完全不像是错误。https://api.example.com//v1/users和https://api.example.com/v1/users在肉眼上几乎一样但相当一部分网关和框架会把双斜杠当成不同的路径处理。结尾斜杠同理有些框架认为/users和/users/是两个不同的路由有些则会自动重定向而重定向在跨域场景下又会引发新的问题。我在项目里一般会写一个统一的拼接函数把所有路径都过一遍function joinUrl(base, path) { if (!base) return path; return ${base.replace(/\/$/, )}/${path.replace(/^\//, )}; }逻辑很直白先去掉 base 结尾的所有斜杠再去掉 path 开头的所有斜杠最后用单个斜杠连接。这样无论调用方传进来的是/users、users还是//users结果都是稳定的。还有几个和路径有关的坑值得单独列出来。一是大小写Linux 文件系统和很多后端路由是区分大小写的本地 macOS 或 Windows 上跑得好好的一上服务器就 404就是这个问题。二是 URL 编码路径里带中文或者空格时不同 HTTP 客户端对%20和的处理不一致。三是环境变量末尾的斜杠.env里写API_BASEhttps://api.example.com/v1/代码里又拼一次/users双斜杠就出来了而且只在部分环境出现非常难查。提示环境变量里的基址一律不写结尾斜杠把它作为一条团队约定固定下来比事后到处修拼接代码省事得多。3.3 代理与网关转发类斜杠写反流量就走了岔路开发环境用 Vite 或 webpack 的 devServer proxy生产环境用 Nginx这两处的配置错误导致的 404 占比极高。先说开发代理// vite.config.js export default { server: { proxy: { /api: { target: http://127.0.0.1:8080, changeOrigin: true, rewrite: (p) p.replace(/^\/api/, ) } } } }这个配置的意思是前端请求/api/users代理转发到http://127.0.0.1:8080/users。注意rewrite这一行——如果后端真实路径是/api/users后端自己也带/api前缀那这行 rewrite 就是多余的加了反而会把路径改错导致 404。这两种约定在团队间经常混用接手新项目时第一步就该确认清楚后端的路由前缀到底是什么。Nginx 的proxy_pass有个非常经典的行为差异我几乎每次带新人都要讲一遍# 情况一不带尾部斜杠原样转发 location /api/ { proxy_pass http://backend:8080; } # /api/users - http://backend:8080/api/users # 情况二带尾部斜杠替换掉 location 匹配的部分 location /api/ { proxy_pass http://backend:8080/; } # /api/users - http://backend:8080/users差别就在proxy_pass后面那个斜杠上一个转发后保留/api一个把/api干掉。如果你看到的现象是接口明明写了/api/users后端日志里收到的却是/users那八成就是这里多写了一个斜杠。另外还要检查location的匹配规则本身。location /api和location /api/匹配范围不同前者能匹配/apixxx后者只匹配/api/开头。还有location /api这种精确匹配只匹配完全相同的路径多一个字符都不行。这几个写法混在一起时优先级顺序是精确匹配 前缀匹配中最长的 正则匹配配错一个就可能让请求落到默认的location /上然后返回 404。3.4 容器网络与端口类localhost 不是你以为的那个 localhost把应用容器化之后这类问题会突然变多。核心原因是网络命名空间隔离每个容器有自己独立的localhost指向容器自己。所以容器里的应用去连localhost:3306连的是这个容器内部的 3306不是宿主机的数据库。正确的做法是在同一个 compose 网络里用服务名互相访问services: web: build: . environment: - API_BASEhttp://backend:8080 backend: build: ./server ports: - 8080:8080这里web服务里写http://backend:8080Docker 的内置 DNS 会把backend解析到对应容器的地址。如果写成localhost:8080那就要看 web 容器自己有没有在 8080 上监听服务了通常是没有的于是连接被拒——这个场景下报的错就是Failed to fetch而不是 404因为压根没连上。还有一类是端口映射写错。ports里的8080:8080是宿主机端口:容器端口如果写成9090:8080那你从宿主机访问localhost:9090才对。很多人在浏览器里一直刷localhost:8080自然全是 404 或者连接失败。部署到集群环境时还要多看一层入站规则、健康检查路径、以及实际监听的端口是否和配置一致。我遇到过一次应用配置里监听的是 3000但容器编排配置里暴露和探活的是 8080导致服务一直在被判定为不健康然后反复重建从外面看就是间歇性 404。3.5 上游能力不存在类对方已经告诉你答案了调用第三方接口时返回的 404通常带着非常明确的文字说明。比如提示某个模型不存在、某个文档路径找不到、某个端点未开放。这类返回里url字段往往也会一起打出来那正是排查的第一手材料。处理这类问题的顺序建议是先把完整 URL 复制出来逐个片段核对——域名对不对、版本号在不在、路径是不是从文档里原样抄的、查询参数有没有漏。然后确认鉴权信息是否传递到位因为有一部分服务出于资源保护的考虑对无权访问的资源会返回 404 而不是 403目的是不暴露资源是否存在。这种设计在业界是常规做法遇到时不要只盯着路径找问题。还有一种情况是环境基址搞混了。测试环境、生产环境的域名不一样如果基址是通过环境变量注入的很容易在某个环境里注入成了另一套地址于是请求打到了没有该资源的服务上。我一般的做法是在启动日志里把实际生效的基址打出来一眼就能看出对不对比出问题后再翻配置快得多。4. 三个完整案例的实操复盘理论讲完了下面三个案例都是我在实际项目里遇到过的我把当时的排查路径和最终修法完整复述一遍你可以对照自己的场景看。4.1 案例一本地开发接口 404代理 rewrite 多改了一层现象是这样的前端调/api/v1/order/list浏览器 Network 里 Status 是 404响应体是一段 HTML 错误页Type 是xhr。Vite 终端没有报错后端日志里压根没有这条请求记录。第一步我先把 cURL 复制出来把 URL 里的/api前缀去掉直连后端curl -i http://127.0.0.1:8080/v1/order/list返回 200说明后端路由是/v1/order/list不带/api。这证明后端本身没问题问题在代理层。第二步看 Vite 配置rewrite那行确实把/api去掉了看起来没问题。但再往下看代理的target写的是http://127.0.0.1:8080/末尾带了一个斜杠。Vite 的 http-proxy 在 target 带路径或末尾斜杠时的行为会跟预期不一致导致改写后路径变成//v1/order/list后端收到双斜杠路由匹配失败返回 404。修法就是把 target 末尾的斜杠去掉target: http://127.0.0.1:8080改完重启 dev server请求正常。这个案例给我的教训是代理配置里的每一个斜杠都要当成有语义的东西来看不要当成格式问题随手加。4.2 案例二部署后刷新页面 404history 路由没做兜底第二个案例是典型的部署问题。本地开发一切正常用npm run dev点来点去都没事打包上服务器之后从首页点进详情页正常一按 F5 就出现服务器返回的 404 页面。排查只用了一分钟Network 面板里那条 404 的 Type 是document响应体是 Nginx 的默认错误页没有任何应用层信息。这说明请求没有到达应用是 Nginx 在文件系统里没找到对应文件。打开 Nginx 配置一看静态托管的location /里只有root和index没有try_files。加上兜底就解决了location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }这个案例有个延伸客户的站点部署在/portal/子路径下。这时候try_files的兜底也得改成/portal/index.html同时前端构建的base要设成/portal/否则就算兜底返回了index.html里面的资源引用还是会指向根目录导致脚本加载 404页面白屏但状态码是 200看起来更迷惑。我现在的习惯是静态托管的配置模板里直接写上try_files不管当前项目是不是 history 路由先加上再说成本几乎为零。4.3 案例三调用第三方返回 404 JSON模型名和基址同时错了第三个案例是调用外部接口时出现的。返回体是这样的结构状态码 404body 里是一段 JSON提示请求的某个模型不存在或不可用同时带上了请求的 URL。看到这种返回第一步不要改代码先把这段 JSON 完整读一遍。这类提示通常已经把原因写在字面上了。当时的情况是代码里使用的模型标识写成了一个带版本后缀的名称而服务方实际提供的标识不带那个后缀两者对不上对方直接判定资源不存在。第二个问题出在基址上。项目里有两套环境变量一套指向测试环境一套指向正式环境而构建脚本里注入的是测试环境的值但模型名用的是正式环境的。两个环境的能力清单不一样于是测试环境的地址加正式环境的模型名必然 404。修法是两步把模型标识改成服务方文档里原样给出的值不做任何合理推测把基址通过启动日志暴露出来每次部署后确认一次实际生效的值。第二条尤其重要因为它能防止同类问题再次发生而不是只修好这一次。提示对于外部接口返回的结构化错误先把url和message两项打全再动手改代码能省下大量猜测时间。5. 速查表与避坑经验前面讲的都是怎么查这一节整理一些可以贴在显示器边上的速查内容和几条硬规矩都是反复踩坑之后沉淀下来的。5.1 症状到动作的速查表现象大概率原因第一时间该做什么Network 里无请求记录控制台 Failed to fetch跨域被拦、混合内容、请求被拦截器取消看 Console 完整报错检查协议是否 HTTPS 页面请求 HTTPStatus 404Type 是 document前端路由未做服务端兜底检查try_files或对应平台的重写规则Status 404Type 是 script/style构建 base/publicPath 配错对比请求路径与部署目录修正 basecurl 正常浏览器 404代理改写、Service Worker 缓存、扩展干扰无痕窗口重试关闭 Service Worker本地正常服务器 404路径大小写、环境变量差异核对文件名大小写打印生效的环境变量返回 JSON 且含 detail/not found上游资源不存在或无权访问核对路径、版本号、模型标识、鉴权头容器内报连接失败或 404localhost 指向错误、端口映射错改用服务名互访核对 ports 映射这张表我基本是照着真实工单整理的覆盖了日常八成以上的情况。剩下两成比较特殊通常需要看链路日志和网关配置才能定位。5.2 我踩过的坑与几条硬规矩第一条规矩任何从浏览器复制的 URL都要检查首尾有没有空白字符和不可见字符。这类字符在 DevTools 里看不出来粘贴到代码或配置文件里就会变成路径的一部分导致 404。我遇到过不止一次最后是靠cat -A才发现的。第二条缓存导致的 404 是最难查的一类因为你会反复确认配置、反复重启每次都看起来是对的。要排查它先开无痕窗口再关闭 Service Worker最后在 Network 面板勾上Disable cache。如果这样就好了那问题就在缓存策略上得去检查 Service Worker 的缓存名单和 CDN 的缓存规则。第三条CDN 有可能把一个 404 响应缓存下来。CDN 通常默认不缓存 4xx但配置不当的时候会缓存。表现是我明明已经修好了访问还是 404。这时候需要在响应头里确认缓存命中状态必要时主动刷新缓存。第四条改完配置一定要真正重启服务而不是 reload。代理配置、环境变量这两类改动很多工具不会热更新看起来改完了其实没生效于是你会得出改了也没用的错误结论。第五条把请求的完整信息带进日志。我在项目里统一要求接口失败时必须记录状态码、完整 URL、请求方法、以及响应体的前 500 个字符。有了这几项绝大部分问题不需要重现场景就能定位。5.3 一个容易被忽略的检查点请求方法严格来说方法不匹配返回的是 405但实际项目里因为网关统一处理或者框架配置的原因报 404 的情况并不少见。我遇到过前端用 GET 请求一个只接受 POST 的接口网关没做方法校验直接按路径找不到对应处理而返回 404。所以排查时顺手确认一下请求方法成本很低在 Network 面板的请求详情里看 Request Method和后端路由定义对一下。这个动作两秒钟但能避免你在路径上死磕。6. 让这类问题少复发请求层的三个工程化习惯排查能力再强也不如让问题少发生。这一节是我在项目里落地过的三个习惯都不复杂但对减少这类问题很有帮助。6.1 请求封装与错误归一化不要在业务代码里直接用裸fetch也不要让封装层把 404 转成一句无信息的错误。我推荐的做法是统一成一个错误对象至少保留status、url、method、bodySnippet四个字段async function request(url, options {}) { const res await fetch(url, options); const text await res.text(); if (!res.ok) { const err new Error(HTTP ${res.status}); err.status res.status; err.url url; err.method options.method || GET; err.bodySnippet text.slice(0, 500); throw err; } return text ? JSON.parse(text) : null; }这里刻意先取text再决定要不要解析 JSON原因很实际404 的时候返回体经常是 HTML直接用res.json()会抛出一个语法错误把真正的 404 信息盖掉控制台里就只剩一句莫名其妙的解析失败。先拿文本再按需解析能保住原始信息。这个改动看起来很小但它让后续所有排查都有据可依不用再去猜。6.2 路径常量与环境基址收敛第二件事是把所有接口路径集中到一个地方管理别散落在几十个文件里。我一般会建一个api.js把所有路径写成常量拼接统一走joinUrl。环境基址只从环境变量读一次且约定不带尾部斜杠。这样做的收益在排查时特别明显你只需要检查一个文件就能确认路径对不对而不是全局搜索/api/然后一个个比对。对于多环境项目我还会在应用启动时把生效的基址打到控制台或者日志里一秒钟确认环境对不对。6.3 上线自检清单最后一件事是给部署加一个三分钟的检查环节我把它固化成清单首页能打开刷新首页不出现服务器错误页。进入任意一个二级路由按 F5 刷新不出现 404。打开 Network确认所有 JS 和 CSS 资源的状态都是 200没有 404。触发一次真实接口调用确认状态 200 且返回结构正确。在日志里确认生效的环境基址是本次目标环境的地址。如果用了缓存或 CDN用无痕窗口再走一遍第 1 到第 4 步。这六条里有任何一条不通过就不要继续往下发。我见过太多次本地测过了就直接上结果是一个 history 路由的兜底配置漏了用户一刷新就是 404流量直接掉一截。我个人在这类问题上的体会是它几乎没有技术难度全是细节。真正拉开差距的不是谁知道 404 是什么意思而是谁能在十分钟内判断出这个 404 是五类里的哪一类然后直奔对应的位置去改而不是从浏览器一路翻到数据库。把事实先固定下来、把想法先放一放这个顺序坚持下去你会发现绝大多数看起来玄学的 Failed to fetch 加 404其实都有非常朴素的解释。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价