OPTIONS预检必须返回204而非200,因RFC与Fetch标准强制要求;Gin中需用c.AbortWithStatus(204)提前终止,避免后续handler执行;Credentials为true时Origin不可用*,须动态白名单校验。

为什么 OPTIONS 请求返回 200 却没后续请求
这不是前端 bug,是服务端响应状态码不合规。浏览器对预检请求的响应有严格要求:204 No Content 是唯一被广泛接受的成功状态;200 OK 虽然能通过部分测试,但在 Chrome、Safari 等主流浏览器中会导致静默失败——即预检看似成功(Network 面板显示 200),但后续真实请求被拦截且无错误提示。
根本原因在于 RFC 7468 和 Fetch 标准明确要求:预检成功必须返回 204,且响应体为空。Gin 中若用 c.AbortWithStatus(200) 或 c.Status(200),即使设置了所有 CORS 头,也大概率失效。
- 不要手动拼
200响应,更不要调用c.JSON(200, ...) - 必须用
c.AbortWithStatus(204)或c.Status(204)+c.Next()之后立即终止流程 - 确保
Content-Length: 0和Content-Type不被意外设置(Gin 默认不会设,但自定义中间件可能干扰)
Gin 中间件里 OPTIONS 处理必须提前 abort
很多人把 CORS 中间件写成“先设 header,再判断 method,最后 c.Next()”,这在 OPTIONS 请求下会出问题:header 虽已写入,但后续 handler 仍被执行,可能导致 panic(比如空 body 的 POST handler 试图解析 JSON)或返回非预期内容。
正确做法是:检测到 OPTIONS 后立刻响应并中断中间件链,不让请求继续向下走。
- 用
c.Request.Method == "OPTIONS"判断,不要依赖路由匹配结果 - 响应前必须调用
c.Abort()或c.AbortWithStatus(204),否则c.Next()仍会执行后续 handler - 推荐统一写法:
c.Header("Access-Control-Allow-Origin", origin); c.AbortWithStatus(204) - 避免在
c.Writer.Header().Set(...)后还调用c.JSON或c.String,那会触发重复写 header 报错
Access-Control-Allow-Credentials 和 Origin 不能共存通配符
当前端携带 Cookie 或 Authorization header 跨域时,后端必须同时设置 Access-Control-Allow-Credentials: "true" 和明确的 Access-Control-Allow-Origin 值。但若 Origin 设为 "*",浏览器会直接拒绝整个响应——连预检都过不去。
这是硬性安全限制,不是 Gin 的 bug,也无法绕过。
- 开发环境可固定写死:
c.Header("Access-Control-Allow-Origin", "http://localhost:3000") - 生产环境必须动态校验:
origin := c.Request.Header.Get("Origin"),再白名单比对 - 绝对不要写
c.Header("Access-Control-Allow-Origin", "*")+c.Header("Access-Control-Allow-Credentials", "true") - 如果不需要凭据(比如纯 public API),可去掉
credentials设置,此时"*"才合法
Nginx 层拦截 OPTIONS 比 Gin 更高效
如果你的 Gin 服务前面有 Nginx,优先让 Nginx 吃掉 OPTIONS 请求。它比 Go 进程处理快一个数量级,且完全规避了 Go runtime 分配、中间件调度等开销。
关键点不在“能不能”,而在“要不要让 Go 处理”——绝大多数业务场景下,OPTIONS 不需要业务逻辑,交给网关最合理。
- Nginx 配置中用
if ($request_method = 'OPTIONS') { ... return 204; }直接终结 - 确保
add_header在 if 块内生效(Nginx ≥ 1.7.0),旧版本需改用map+ 全局add_header - 对应地,Gin 中应移除所有 CORS 中间件,或至少禁用 OPTIONS 处理分支
- 注意:Nginx 返回的
Access-Control-Allow-Origin必须与前端实际 Origin 完全一致,否则浏览器仍会拒收
真正容易被忽略的是:预检响应的 Access-Control-Max-Age 缓存时间一旦设得太长(比如 1 小时),后续修改 CORS 配置后前端不会立刻生效,得等缓存过期或强制刷新。线上调试时建议先设成 60,验证无误再调高。


















