带X-Trace-ID或X-Auth-Token的请求卡在OPTIONS预检,因浏览器对自定义头发起预检而服务端未在Access-Control-Allow-Headers中显式声明这些头;需在gin-contrib/cors的AllowHeaders中逐个写死且大小写严格匹配,动态头场景须手写中间件解析Access-Control-Request-Headers并响应Access-Control-Allow-Headers;同时前端读取响应头需配置ExposeHeaders,否则getResponseHeader返回null。

为什么带 X-Trace-ID 或 X-Auth-Token 的请求总卡在 OPTIONS 预检?
因为浏览器看到自定义请求头(非 Accept、Content-Type 等简单头)会自动发 OPTIONS 预检,而服务端若没在响应中明确声明允许该头,预检就失败。现象是控制台报错 Request header field x-trace-id is not allowed by Access-Control-Allow-Headers,后续实际请求根本不会发出。
AllowHeaders 必须显式列出所有前端用到的自定义头
gin-contrib/cors 的 AllowHeaders 不会自动继承或通配,必须逐个写死。漏一个,预检就挂。
- 错误写法:
AllowHeaders: []string{"Content-Type"}—— 即使前端只多传了X-Trace-ID,也会失败 - 正确写法:
AllowHeaders: []string{"Content-Type", "X-Trace-ID", "X-Auth-Token", "Authorization"} - 注意大小写:浏览器发送的是
X-Trace-ID,你就得写"X-Trace-ID",不能写成"x-trace-id"(Go 的 Header 是大小写敏感的) - 如果用了
Access-Control-Request-Headers里没出现的头,哪怕后端允许,浏览器也不会发——所以前端也要自查发了哪些头
当需要动态头(比如按租户白名单不同)时,gin-contrib/cors 不够用
这个库不支持运行时解析 Access-Control-Request-Headers 并做条件判断。它只在初始化时静态匹配。
- 如果你的 API 要支持任意租户自定义头(如
X-Tenant-ID+X-Env组合),别硬套AllowHeaders列表 - 改用手写中间件,在
c.Request.Method == "OPTIONS"时读取c.Request.Header.Get("Access-Control-Request-Headers"),再按业务规则校验并回写Access-Control-Allow-Headers - 示例关键逻辑:
if c.Request.Method == "OPTIONS" {<br> headers := c.Request.Header.Get("Access-Control-Request-Headers")<br> if isValidCustomHeader(headers) {<br> c.Header("Access-Control-Allow-Headers", headers)<br> }<br> c.AbortWithStatus(204)<br> return<br>}
ExposeHeaders 和前端 getResponseHeader() 的关系
前端 JS 想通过 response.getResponseHeader("X-Trace-ID") 拿服务端返回的自定义响应头,光写 c.Header("X-Trace-ID", "abc123") 不够——必须把该头加进 ExposeHeaders,否则 JS 读不到。
- 遗漏后果:响应里明明有
X-Trace-ID,但getResponseHeader返回null - 配置示例:
ExposeHeaders: []string{"X-Trace-ID", "X-Rate-Limit-Remaining"} - 注意:
ExposeHeaders不影响跨域权限,只控制 JS 是否能读取;它和AllowHeaders是两套独立配置
AllowHeaders 控制预检,ExposeHeaders 控制响应读取,AllowCredentials 和 AllowOrigins 的组合又牵扯 cookie 传递——这三者稍有不匹配,问题就会在不同阶段暴露,排查时得盯住 Network 面板里 OPTIONS 和后续请求的响应头分别缺了什么。


















