现象:
前端同学在请求接口时新增了一个 Header(如Xp-Log-Id),
浏览器立刻报 CORS 跨域错误,
后端 & Nginx 日志 完全没有请求记录。
这类问题非常常见,但也极容易被误判为“前端问题”或“浏览器问题”。
本文从 规范原理 → 真实链路 → 可落地方案,一次性讲清楚。
一、问题背景
原始请求(正常)
POST /Publics/index
Content-Type: multipart/form-data前端新增 Header 后(出问题)
POST /Publics/index
Content-Type: multipart/form-data
Xp-Log-Id: 1766558867456wbdfjpr浏览器报错示例:
Access to fetch at 'https://api.xxx.com/...' from origin 'https://web.xxx.com'
has been blocked by CORS policy二、核心结论(一句话)
**只要客户端增加了“自定义 Header”,浏览器一定会触发 CORS 预检(OPTIONS);
如果服务端 / 网关没有正确响应这个 OPTIONS,请求会被浏览器直接拦截,
真正的接口请求根本不会发送。**
三、为什么“加 Header”会导致跨域?
1️⃣ CORS 中的「简单请求」限制
浏览器只对以下请求 不做预检:
- Method:
GET / POST / HEAD Header 只能是:
AcceptAccept-LanguageContent-LanguageContent-Type(仅限):application/x-www-form-urlencodedmultipart/form-datatext/plain
👉 任何额外 Header 都会触发预检
例如:
Authorization
X-Request-Id
Xp-Log-Id
X-Trace-Id2️⃣ 预检请求(OPTIONS)长什么样?
浏览器会先发送:
OPTIONS /Publics/getWebInitInfo
Origin: https://web.xxx.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: xp-log-id, content-type⚠️ 如果这个请求失败,真正的 POST 永远不会发送
四、为什么“X-Request-Id 好像一直没问题?”
这是一个非常常见的误解。
真相只有一个:
**不是X-Request-Id特殊,
而是它“早就被服务端 / 网关允许了”。**
常见原因包括:
- 公司级 Nginx / Gateway 模板里已包含:
Access-Control-Allow-Headers:
Content-Type, Authorization, X-Request-Id- Spring Cloud Gateway / Kong / APISIX 默认白名单
- 浏览器 DevTools 默认折叠了 OPTIONS,请求“看不见”
👉 预检其实一直存在,只是成功了
五、为什么这次新增 Xp-Log-Id 就失败了?
因为:
Access-Control-Request-Headers: xp-log-id而服务端返回的却是:
Access-Control-Allow-Headers: Content-Type, X-Request-Id❌ 不包含 Xp-Log-Id
浏览器校验规则:
Access-Control-Request-Headers ⊆ Access-Control-Allow-Headers➡️ 校验失败
➡️ 浏览器直接拦截
➡️ 后端 & Nginx 都“看不到请求”
六、为什么“只在 server/service 层加 OPTIONS 不生效?”
Nginx 的真实处理顺序
server
↓
location(最精确匹配)
↓
rewrite / return / proxy请求路径:
/Publics/index如果存在:
location /Publics/ { ... }👉 只有在这个 location 内的配置,才一定会生效
七、100% 成功的 Nginx 处理方案(推荐)
✅ 在最终命中的 location 中统一处理 CORS + OPTIONSlocation /Publics/ {
# ===== CORS 统一响应 =====
add_header 'Access-Control-Allow-Origin' $http_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
# ⚠️ 必须包含所有前端可能传的 Header
add_header 'Access-Control-Allow-Headers'
'Content-Type, Authorization, X-Request-Id, Xp-Log-Id' always;
add_header 'Access-Control-Max-Age' 86400 always;
# ===== 预检请求直接返回 =====
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://backend;
}关键点总结
- ✅ 必须在
location - ✅
always不能省 - ✅
Allow-Headers必须包含新增 Header - ✅ OPTIONS 不进后端
八、偷懒但实用的方案(内部系统可用)
add_header 'Access-Control-Allow-Headers'
$http_access_control_request_headers always;优点:
- 前端加任何 Header 都不会炸
缺点:
- 安全粒度较粗
九、给团队的统一认知结论
- CORS 是 浏览器安全策略
- 前端 无法规避 OPTIONS 的产生
- Nginx / 网关 可以 100% 保证预检通过
- “后端没日志” ≠ “请求没问题”
十、最佳实践建议
- 统一定义 Header 白名单
- CORS 放在网关层处理
- OPTIONS 永不进业务代码
- 新增 Header 必须同步检查 CORS
结语
**跨域不是 Bug,而是协议规则。
真正的问题往往不是“加了 Header”,
而是“没人告诉浏览器你允许它”。**