CORS 报错大概是前端最难读的报错之一:一段英文、三层引号嵌套,第一眼根本分不清谁拦了谁。我把这几年项目里踩过的 CORS 报错整理了一遍,一共 8 个,每个都给出现场报错原文、原因和解法,文末有一张可以直接收藏的速查表------下次再遇到,10 秒对号入座。
提前说一个:第 8 个报错去年还不存在。它是 Chrome 正在灰度推进的 Private Network Access 限制,最近社区里"本地环境跑了一年突然报跨域"的求助帖,大半是它干的。
先用30秒把CORS机制过一遍
浏览器有个同源策略:不同源(协议、域名、端口任一不同)的页面之间,默认不允许读取对方的响应。CORS 就是给这个限制开的口子------服务端在响应头里声明"允许哪个 origin 读我",浏览器检查通过才把数据交给 JS。
所以记住第一个结论:CORS 报错几乎都是服务端响应头的问题,不是你前端代码的问题。你的请求大概率发出去了,服务端也正常返回了,只是响应头里没有那句"允许"。
请求还分两种:
- 简单请求:GET/HEAD/POST + 简单头(没有自定义头、Content-Type 只限表单那几种),浏览器直接发,回来检查响应头。
- 预检请求:带自定义头、用了 PUT/DELETE、Content-Type 是 application/json 之类的,浏览器会先发一个 OPTIONS 请求"问路"------服务端在预检响应里答"我允许这些方法和头",通过了才发真实请求。
报错也就分成两类:简单请求查响应头,预检请求查 OPTIONS 响应。下面 8 个报错,对号入座。
报错1:没有Access-Control-Allow-Origin头(最常见)
报错原文:
csharp
Access to fetch at 'https://api.example.com/data' from origin
'https://app.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
含义很直白:响应头里压根没有 Access-Control-Allow-Origin。解法分三档:
1)能改服务端,直接加头。 Express 示例:
js
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
next();
});
2)本地开发改不了后端,用构建工具代理。 Vite 示例:
js
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': { target: 'https://api.example.com', changeOrigin: true },
},
},
});
配好之后前端直接请求 /api,dev server 帮你转发到后端。浏览器看到的是同源请求,跨域问题从根上消失------这也是本地开发最推荐的方式。
3)线上环境、服务端又不归你管,只能让你自己的服务端加一层转发,别让浏览器直接碰对方接口。
报错2:带cookie时不能用通配符*
报错原文:
ruby
The value of the 'Access-Control-Allow-Origin' header in the response
must not be the wildcard '*' when the request's credentials mode is 'include'.
前端开了 credentials: 'include'(要带 cookie)之后,服务端就不能偷懒用 Access-Control-Allow-Origin: * 了,必须回显具体的 origin,而且还要带上 Access-Control-Allow-Credentials: true。正确姿势是服务端动态判断白名单:
js
const allowList = new Set(['https://app.example.com']);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowList.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
next();
});
注意:这个场景下 Access-Control-Allow-Headers 和 Access-Control-Allow-Methods 同样不接受 *,都得写具体值。
报错3:Allow-Origin出现了多次
报错原文:
sql
The 'Access-Control-Allow-Origin' header contains multiple values.
这个头在响应里只能出现一次、只能有一个值。典型成因是两层都配了:nginx 里 add_header 加了一次,应用代码里又 setHeader 了一次,浏览器收到两个值直接拒绝。
解法:全链路排查 nginx、网关、应用框架三层,只保留一处。排查时可以打开 DevTools 的 Network 面板看响应头原文,重复的会明晃晃地出现两行。
报错4:预检不允许该请求方法
报错原文:
csharp
Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.
前面说过,DELETE 属于复杂请求,浏览器会先发 OPTIONS 问路。服务端的预检响应里 Access-Control-Allow-Methods 没列 DELETE,请求在问路阶段就被毙了,真实请求根本没发出去。
解法是把用到的方法补全。Express 示例:
js
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,X-Token');
res.setHeader('Access-Control-Max-Age', '86400');
return res.status(204).end();
}
报错5:预检不允许自定义请求头
报错原文:
vbscript
Request header field x-token is not allowed by Access-Control-Allow-Headers in preflight response.
你给请求加的每一个非简单头------Authorization、自定义的 X-Token------都要在服务端的 Access-Control-Allow-Headers 里逐个列出来,一个都不能少。
一个容易忽略的连带问题:很多 HTTP 库会默认把 Content-Type 设成 application/json,这本身就会把请求升级成复杂请求、触发预检。如果你的接口其实只收表单数据,把 Content-Type 改回 application/x-www-form-urlencoded 可以省掉整轮预检。
报错6:配置明明改了,浏览器还报老错
这个没有新的报错文本,症状是"报错4、报错5都修好了,浏览器还在报一模一样的错"。
原因:预检结果是可以缓存的,Access-Control-Max-Age 就是缓存时长。之前配置宽松时设过 86400(一天),那你改完配置后的一天内,浏览器都会拿旧的预检结果直接用,根本不再问路。
解法:调试期间把 Access-Control-Max-Age 设成 0,或者在 DevTools 里勾上 Disable cache,再测。
报错7:重定向的目标没有CORS头
报错原文(注意里面出现了两个 URL):
csharp
Access to fetch at 'https://b.example.com/data' (redirected from
'https://a.example.com/api') from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header
is present on the requested resource.
CORS 检查的是最终响应。如果接口 302 跳到了另一个域------常见的有跳登录中心、跳 CDN、跳网关------那么最终那个域的响应也必须带 CORS 头。前端对这种错无解,只能让服务端处理:要么去掉重定向,要么让重定向目标也配上头。
报错8:Chrome Private Network Access(去年还不存在的新坑)
报错原文和传统跨域长得很像,但会多出一句关键信息:
swift
The request client is not a secure context and the resource is in
more-private address space `local`
或者预检阶段的:
csharp
No 'Access-Control-Allow-Private-Network' header is present on the
requested resource.
背景:Chrome 正在分阶段推进 Private Network Access(私有网络访问)限制------公网页面想访问 localhost、内网 IP 这种"更私有"的网络资源,必须先通过预检,而且服务端要额外返回一个头:
vbnet
Access-Control-Allow-Private-Network: true
同时页面本身得处于安全上下文(https 或 localhost)。这就是最近"本地环境跑了一年都没事、突然开始报跨域"的原因:不是你的代码变了,是浏览器的规则在收紧。
应对分场景:
- 本地开发:把页面也放在 localhost 下跑(同源同为本地,直接绕开),是最省事的。
- 必须公网访问本地服务(比如真机调试、内网部署的调试工具):给本地服务加上面那个响应头,并确保页面是 https。
- 临时应急:在 chrome://flags 里搜索 Private Network Access,把相关拦截开关临时关掉------只适合自己调试,别写进文档让用户做。
这个限制还在灰度推进中,建议现在就把它加进排查清单,不然哪天团队里总有人会来问你"为什么突然全挂了"。
CORS报错速查表
| 报错关键词 | 原因 | 解法 |
|---|---|---|
| No 'Access-Control-Allow-Origin' header | 服务端没返回该头 | 服务端加头 / 构建工具代理 / 服务端转发 |
| must not be the wildcard '*' | 带 cookie 却用了通配符 | 动态回显白名单 origin + Allow-Credentials |
| contains multiple values | 同一个头被配置了两层 | 排查 nginx/网关/应用,只留一处 |
| not allowed by Access-Control-Allow-Methods | 预检没放行该方法 | Allow-Methods 补全方法 |
| not allowed by Access-Control-Allow-Headers | 自定义头没被列出 | Allow-Headers 逐个补上 |
| 配置改了还报旧错 | 预检结果被 max-age 缓存 | max-age 设 0 或禁用缓存再测 |
| redirected from ... | 重定向目标没 CORS 头 | 服务端去掉重定向或给目标加头 |
| more-private address space | Chrome PNA 私有网络限制 | 加 Allow-Private-Network: true + 安全上下文 |
三个常见误区
误区1:mode: 'no-cors' 能解决跨域。 不能。它只是让请求不报错,返回的是一个 opaque response------状态码读不到、响应体读不到,等于发了个寂寞。它唯一的正当用途是埋点上报这种"不需要读响应"的场景。
误区2:装个 Allow CORS 浏览器插件就行。 插件只是给你本机浏览器的响应注入头,线上用户的浏览器里没有任何插件。它只适合本地调试救急,把它当解决方案,上线必翻车。
误区3:用 JSONP 绕过去。 JSONP 只支持 GET、要后端专门配合、还存在注入风险,现在几乎没有新项目在用。都这个年代了,老老实实走标准 CORS。
写在最后
CORS 报错看着吓人,本质上只有三类:服务端没给头、头的值不对、浏览器加了新规则(PNA 就是第三类)。下次看到报错,先复制报错文本到上面的速查表里对号入座,别上来就乱加头碰运气------乱加头只会把报错1变成报错3。
你踩过哪个 CORS 报错?或者最近也遇到了"突然跨域"的灵异事件?评论区聊聊。速查表有用的话帮我点个收藏,后面遇到新报错我会继续往里补。
我是把报错原文整理成速查表的前端,关注我,下次报错不慌。