Karp 的技术博客
现象
前端同学在请求接口时新增了一个 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 只能是:

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(仅限):

      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain

👉 任何额外 Header 都会触发预检

例如:

Authorization
X-Request-Id
Xp-Log-Id
X-Trace-Id

2️⃣ 预检请求(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 + OPTIONS
location /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% 保证预检通过
  • “后端没日志” ≠ “请求没问题”

十、最佳实践建议

  1. 统一定义 Header 白名单
  2. CORS 放在网关层处理
  3. OPTIONS 永不进业务代码
  4. 新增 Header 必须同步检查 CORS

结语

**跨域不是 Bug,而是协议规则。
真正的问题往往不是“加了 Header”,
而是“没人告诉浏览器你允许它”。**

版权属于:karp
作品采用:本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
更新于: 2025年12月24日 12:20
0

目录

来自 《【踩坑】一次“加 Header 就跨域”的完整复盘:原因、原理与 100% 可落地的解决方案》
809 文章数
0 评论量
9 分类数
814 页面数
已在风雨中度过 10年190天19小时16分