中间件注册必须在所有路由之前,即r.use()需紧接gin.default()后、任何r.get()/post()前;否则如cors中间件滞后,options预检将直接404且无日志。

中间件注册顺序必须在所有路由之前
这是最常被忽略的硬性条件。Gin 的 r.Use() 必须紧接在 r := gin.Default() 或 gin.New() 之后、任何 r.GET() r.POST() 等路由注册之前调用。一旦把 CORS 中间件写在路由定义后面,它对 OPTIONS 预检请求完全不生效——因为 Gin 默认不匹配 OPTIONS 路由,没中间件兜底就直接 404,后端日志里甚至不会打印一行。
常见错误写法:
r := gin.Default()
r.GET("/api/user", handler)
r.Use(corsMiddleware()) // ❌ 晚了!OPTIONS 请求根本进不来
正确顺序:
r := gin.Default()
r.Use(corsMiddleware()) // ✅ 第一个 Use()
r.GET("/api/user", handler)
r.POST("/api/user", handler)
中间件内部不能在 c.Next() 前写响应或中断
手写中间件时,如果在 c.Next() 前调用 c.AbortWithStatus(204) 或 c.JSON(),会导致真实请求(如 POST)的响应头被覆盖或丢失。更危险的是:预检请求(OPTIONS)必须返回 204 且带全 CORS 头,但若你提前 c.Abort(),c.Header() 设置的头可能未真正写入响应体。
- 必须把所有
c.Header()放在c.Next()之前,确保 OPTIONS 和真实请求都带上 - 必须用
c.Request.Method == "OPTIONS"判断,并在其中调用c.AbortWithStatus(http.StatusNoContent) - 绝不能在
c.Next()前调用c.Abort()或c.Status(),否则后续 handler 无法执行,真实请求会无响应
gin-contrib/cors 的 AllowOrigins 和 AllowCredentials 冲突
这个组合是静默失败高发区。当配置 AllowCredentials: true 时,AllowOrigins 列表里只要含一个 "*",浏览器就会彻底丢弃响应——Network 面板里连 status 都看不到,只报 Response to preflight request doesn't pass access control check。
正确做法:
- 显式列出完整协议+域名,如
[]string{"https://admin.example.com", "https://app.example.com"} - 不要用
"*",也不要试图用"https://*.example.com"——该库不支持通配符匹配子域 - 若需动态 Origin 校验(比如从 DB 查白名单),别用
gin-contrib/cors,自己写中间件更可控
原生 net/http 服务漏处理 OPTIONS 就等于没跨域
Gin 至少会尝试匹配路由;而纯 net/http 完全不管 OPTIONS,不显式拦截就是 404。很多开发者照搬 Gin 中间件逻辑,只加 Header、不判断方法、不终结请求,结果前端永远卡在预检。
必须同时满足三点:
- 检查
r.Method == "OPTIONS" - 设置
w.Header().Set("Access-Control-Allow-Origin", origin)等全部必要头 - 调用
w.WriteHeader(http.StatusNoContent)并 return,不能留空 body
漏任一环节,浏览器都会拒绝后续真实请求。
调试时最容易被忽略的点:OPTIONS 请求是否真进了中间件?加一行log.Println("OPTIONS hit") 最快验证。很多“配置写了却无效”的问题,根源只是请求压根没走到中间件里。











