必须显式注册OPTIONS路由或提前拦截;Echo不自动响应预检请求,仅靠CORS中间件无效,因其只补头不接管分发,需在所有中间件前统一拦截并返回204及完整CORS头。

OPTIONS预检请求必须显式注册路由或提前拦截
Echo 默认不会自动响应 OPTIONS 请求,哪怕你只注册了 GET 或 POST。浏览器发起跨域非简单请求时,会先发一个无 body 的 OPTIONS 预检,如果服务端没返回 200/204 + 正确 CORS 头,请求就卡住,前端控制台报 No 'Access-Control-Allow-Origin' header is present。
常见错误是以为加了 CORS 中间件 就万事大吉——但 Echo 官方中间件(包括第三方 echo-middleware/cors)大多只处理响应头,不主动拦截并返回 OPTIONS 响应。它等着你把请求放过去,结果后端 handler 里没写 if c.Request().Method == "OPTIONS" 分支,直接 405 或 panic。
- 最稳妥的做法:对每个需要支持跨域的路径,手动注册一个
OPTIONS路由,返回 204 并设好头 - 更省事的做法:在所有路由前加一个中间件,统一拦截
OPTIONS并立即返回,不走后续逻辑 - 别依赖
e.OPTIONS("/path", handler)覆盖所有路径——它不匹配带参数的路由(如/users/:id),也不处理通配前缀(如/api/*)
用中间件提前拦截 OPTIONS 并返回 204
这个中间件必须放在所有其他中间件之前(比如 JWT、日志、CORS),否则请求已经进到鉴权环节,可能被拒绝或 panic。
核心逻辑就三步:判断是否 OPTIONS → 校验 Origin 是否在白名单 → 设置头并返回 204。
- Origin 必须从
c.Request().Header.Get("Origin")动态读取,不能硬写*(尤其当前端带credentials: true) - 响应头至少包含:
Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers、Access-Control-Allow-Credentials - 必须调用
c.NoContent(http.StatusNoContent)或c.String(http.StatusNoContent, ""),并立刻return,否则后续 handler 还会执行 - 不要在
next(c)之后再设 header——响应体已开始写入,header 失效
为什么不能只靠 CORS 中间件?
官方 echo-middleware/cors 或自定义 CORS 中间件,本质只是往响应里塞 header,它不接管请求分发。也就是说:它假设你已经有一个 handler 处理了该路径的 OPTIONS 请求;它只负责“补头”,不负责“响应”。
开箱即用的技能链路由引擎。13 条预定义链覆盖搜索、开发、审查、MLOps、法律、创意等场景,三层路由架构(触发词→SAD反馈→DAG编排),recall@10=96.97%。配置驱动(chains.yaml),零代码扩展。pip install skill-weave-chains 一键安装。
这导致两个典型问题:
- 你注册了
e.GET("/api/users", handler),但没注册e.OPTIONS("/api/users", ...)→ 浏览器发 OPTIONS,Echo 找不到路由,直接 404 或 405 - 你用了
jwt.Middleware全局注册 → OPTIONS 请求被送去验证 token,而它没 Authorization header,直接 401,根本到不了 CORS 中间件 - 你用
Route::group()->allowCrossDomain()(这是 ThinkPHP 的写法,Echo 没这 API)——Echo 没内置类似机制,强行套用会失效
动态路径(如 /users/:id)的 OPTIONS 怎么处理?
不能为每个参数化路径单独写 e.OPTIONS("/users/:id", ...),那样维护爆炸。正确做法是用中间件 + 路径匹配逻辑。
示例思路:
- 中间件里用
c.Path()拿到当前请求路径(如/users/123) - 用正则或前缀判断是否属于某类 API(如匹配
^/api/.*$) - 只要匹配,就统一返回 204 + CORS 头,无需关心具体参数值
- 注意:不要在中间件里调
c.Param("id")—— OPTIONS 请求没有路由参数解析上下文,会返回空或 panic
真正容易被忽略的是:OPTIONS 请求永远不携带 body,也几乎不触发任何业务逻辑。把它当成一个纯协议层握手信号来对待,别试图复用业务 handler,也别在里面查数据库或调外部服务。










