西西软件园多重安全检测下载网站、值得信赖的软件下载站!
软件
软件
文章
搜索

首页 → 业内动态 → 业内资讯 → 一个 POST 变成两发:跨域预检 OPTIONS 卡在 Nginx

一个 POST 变成两发:跨域预检 OPTIONS 卡在 Nginx

相关软件相关文章发表评论 来源:本站整理时间:2026/11/3 9:32:08字体大小:A-A+

轻量级网页服务器(nginx)v1.19.10 官方最新版
  • 类型:服务器区大小:1.7M语言:英文 评分:6.3
  • 标签:
立即下载

上周调接口的时候,我盯着浏览器 Network 面板看了快十分钟:一个普通的 POST 提交,硬是发成了两次请求第一次是 OPTIONS,返回 200 但什么数据都没有;第二次才是真正的业务 POST,然后被浏览器拦下来,控制台甩一句 Access-Control-Allow-Origin 缺失。前端同事说"接口在 Postman 上完全正常",后端说"我 curl 也是通的",两边都没错,错的是那多出来的第一次请求没被服务端接住。

如果你也正为跨域报错头疼,或者打算自己搭一套前后端分离的环境,可以先看看 ISVORO 全球云服务器(香港/美国/德国/英国多节点,35 秒交付) 这类机器的报价和技术说明,再 云途(服务器运维与跨境加速),环境先跑起来,问题自然好定位。

先搞清楚:那个 OPTIONS 到底是谁发的

这不是浏览器发神经,是它按规范做的预检(preflight)检查。当你的前端页面和接口不在同一个源(协议 + 域名 + 端口三者完全一致才算同源)时,只要这次请求属于"非简单请求"比如带了 Content-Type: application/json、带了 Authorization 请求头、或者方法不是 GET/HEAD/POST浏览器就会先自动发一个 OPTIONS 请求过去,问服务器三件事:这个来源允不允许?这些请求头允不允许?这些方法允不允许?

服务器必须在这个 OPTIONS 响应里明确回上 Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers,浏览器才会把第二个真正的 POST 放出去。关键点在于:这个 OPTIONS 是浏览器直接发给接口服务器的,根本不经过你的前端代码,所以很多人在 axios 里加拦截器、在 fetch 里加 mode,都没用你压根拦不到它。

浏览器跨域预检 OPTIONS 请求流程示意图

最容易踩的三个坑

第一,Nginx 把 OPTIONS 请求吃掉了。 这是最常见的一种。生产环境前面挂一层 Nginx 做静态资源和反向代理,很多配置里只写了 location /api/ { proxy_pass http://backend; },后端框架(比如 Spring Boot、Flask、Django)默认对 OPTIONS 请求返回 405 Method Not Allowed,或者返回了个空 body 且没带任何跨域响应头。前端开发者看到的就是"请求发出去了但没有响应",或者拿到一个空的 200。无论是哪种,浏览器都会判定预检失败。

第二,响应头少了一个字母。 Access-Control-Allow-Origin 写成 Access-Control-Allow-Origins,或者多了个 s;Access-Control-Allow-Headers 里写了 Content-Type 却漏了实际用到的 Authorization。这些在 Nginx 配置里手写的时候特别容易翻车,因为配置文件不报错,Nginx 也不会提醒你。

第三,带了 Cookie 却没设 Access-Control-Allow-Credentials: true。 这个是登录态相关接口的高频翻车点。只要前端请求带了 withCredentials: true,服务端就必须显式回这个头,同时 Access-Control-Allow-Origin 不能再用 * 通配符,必须写成具体的域名。很多人在这里直接把 * 改成域名了却忘了加 Credentials 的那行,结果依旧被拦。

Nginx 反向代理配置跨域响应头示意

能直接用的 Nginx 配置

最省事的做法是让 Nginx 统一处理预检,而不是甩给后端。核心思路是:用一个 if ($request_method = 'OPTIONS') 的分支提前 return,把该回的头一次回全,然后 add_header ... always; 补上真正业务响应里也需要的头。注意 add_header 在 location 块里会覆盖父级的所有 add_header 指令,所以不要把 CORS 头拆在两个层级各写一半,宁可写全在一处。

另外有个容易忽略的细节:add_header 默认只对 200、201、204、206、301、302、303、304、307、308 这些状态码生效。万一后端在某些情况下返回了 4xx 或 5xx,跨域头就会整个消失,前端看到的是一个完全没有 CORS 头的错误响应,排查起来比正常报错还费劲。所以 add_header 后面一定要带 always。

排查顺序,别一上来就改代码

我现在的固定动作是:先在浏览器 Network 里确认那个 OPTIONS 请求的状态码和响应头。如果 OPTIONS 返回 404,说明 Nginx 的 location 没匹配上(常见于路径末尾斜杠不一致);返回 405 或者 200 但没有 CORS 头,说明请求到了后端但后端没处理;如果 OPTIONS 返回了完整的 CORS 头、第二个 POST 却依然被拦,那就轮到检查 Access-Control-Allow-Headers 里有没有列全、以及凭证相关的三个头配没配齐。

还有个土办法但很好用:直接用 curl 模拟预检,把真实浏览器发的请求头原样抄过去:

curl -i -X OPTIONS 'https://api.example.com/api/login' -H 'Origin: https://app.example.com' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: content-type,authorization'

把 curl 的输出和浏览器里的 OPTIONS 响应对比一下,差异往往一眼就能看出来。我个人还有个习惯:把跨域头统一收敛到 Nginx 的 include 文件里统一管理,配一次全站生效,新接口就不用再单独 debug 一次了。

服务器控制台排查跨域请求日志场景

最后提醒一句:开发环境能用 Vite 代理绕开跨域,不代表生产环境没问题。 开发时的 proxy 是同源转发,压根不触发预检,问题被完整地藏到了上线那天才爆出来。所以只要接口和页面不同源,跨域这套东西就得老老实实配,别指望代理救场。

    相关评论

    阅读本文后您有什么感想? 已有人给出评价!

    • 8 喜欢喜欢
    • 3 顶顶
    • 1 难过难过
    • 5 囧囧
    • 3 围观围观
    • 2 无聊无聊

    发表评论

    昵称:
    表情: 高兴 可 汗 我不要 害羞 好 下下下 送花 屎 亲亲