在前后端分离的开发模式已经普及的今天,调试接口时最让人摸不着头脑的,往往不是业务逻辑本身,而是浏览器开发者工具 Network 面板里那个红色的 CORS error。你明明把接口地址填对了,参数也传了,请求却偏偏被拦在半路,更气人的是,后端日志里压根没有你这笔请求的记录。这种时候,十有八九是预检请求出了问题。
预检请求(Preflight Request)本质上是浏览器替我们自动发送的一次 HTTP OPTIONS 请求,它就像小区门口那个从不露面的保安,在你进行跨域请求之前,先拦下来问一句:你打算用什么方法、带哪些头去访问别人家的资源,业主同意了没有?只有在服务端明确放行之后,浏览器才会真正把业务请求发出去。很多新手甚至不少有几年经验的后端同学,都会被这个“多出来”的 OPTIONS 请求搞到怀疑人生。这篇文章我就把预检请求的前因后果、完整握手流程、服务端配置和排查技巧一次讲清楚,里面绝大多数坑都是我实际踩过并解决的,值得你收藏。
1. 预检请求到底是什么:浏览器的“隐形保安”上岗逻辑
在讲预检请求之前,绕不开同源策略。浏览器默认只允许页面里的脚本访问同源资源,所谓同源,指的是协议、域名、端口三者完全一致。哪怕只是从 http://localhost:8080 访问 http://localhost:3000,端口不同,就已经属于跨域,浏览器的安全机制会出手拦截。这个限制本身是为了保护用户数据不被恶意网站盗取,可同时也给正经的前后端联调添了不少堵。
CORS(跨域资源共享)机制就是在这种背景下被设计出来的。它允许服务器通过响应头显式声明“哪些源可以访问我”,浏览器拿到这个声明后,才会放行跨域响应。而预检请求,则是 CORS 机制里一个非常特殊的环节:浏览器在真正发送某些“危险”请求之前,先自动发送一个 OPTIONS 请求去探测服务端的态度。
1.1 为什么需要预检:同源策略这道墙
你可以把同源策略理解成一座小区的大门,门禁默认只放行本小区的业主。CORS 就是物业发放的访客通行证,而预检请求则是门卫在放行之前的电话确认:这个访客要带大件物品进去,我得先问问业主同不同意。
服务端此时还没有收到你的实际业务请求,它收到的只是一次“试探”。如果服务端配置得当,会返回一组 Access-Control-Allow-* 头,相当于在电话里明确说:可以,这个源的人能进,能用 GET 和 POST 方法,可以带 Content-Type: application/json 这种头。浏览器听到这个回答后,才会发出真正的业务请求。否则,浏览器直接拦住请求,并且在后端日志里完全看不到这笔业务请求——响应在浏览器这一层就被中止了,根本到不了服务端。
这个设计其实非常巧妙。它把风险决策前置,避免浏览器在未知服务端态度的情况下,贸然发送可能产生副作用的写操作。想象一下,如果没有什么预检机制,恶意网站可以随意往你的银行账户接口发起转账请求,只要服务端没做额外的防护,后果不堪设想。预检请求至少让服务端有一个机会来验证来者身份和请求合法性。
1.2 什么请求需要预检:简单请求与非简单请求的分界线
并不是所有跨域请求都会触发 OPTIONS 预检。浏览器定义了一类“简单请求”,这类请求被认为不会对数据产生不可预期的副作用,不需要提前探测。满足以下条件,浏览器才会把它当作简单请求直接发送:
- 请求方法仅限于
GET、HEAD、POST三种。 - 请求头只能是浏览器自动生成的那些基础头,加上少部分手动指定的头,比如
Accept、Accept-Language、Content-Language、Last-Event-ID,以及Content-Type为application/x-www-form-urlencoded、multipart/form-data或text/plain。 - 不能使用
XMLHttpRequest或fetch的ReadableStream对象。
一旦超出这个范围,比如使用 PUT、DELETE 方法,或者 Content-Type 用了 application/json,又或者自定义了 Authorization、X-Custom-Header 之类的请求头,浏览器就会先发送一次 OPTIONS 预检请求。
这里有一个非常常见的误区:很多人以为只要跨域了就会发 OPTIONS,其实不是。只发 GET 请求且不带头、不用 JSON 格式,走的是简单请求路线,不会有预检;但是一旦加上 Content-Type: application/json,预检就会立刻出现。这也是为什么前端工程师在联调时经常发现,同样的接口,POST 数据时多了一个 OPTIONS,而 GET 时却看不到。
| 请求情况 | 是否触发预检 | 原因 |
|---|---|---|
| GET 不带自定义头 | 不触发 | 简单请求 |
| POST x-www-form-urlencoded | 不触发 | 简单请求 |
| POST application/json | 触发 | 非简单请求 |
| PUT / DELETE | 触发 | 非简单请求 |
| 携带 Authorization 头 | 触发 | 非简单请求 |
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. OPTIONS预检请求的完整握手过程
清楚了预检的触发条件后,我们来拆解整个握手的细节。这个过程说复杂也不复杂,但每一条请求头和响应头都对应着一次权限校验,少了任何一环,浏览器都会毫不留情地把请求拦下来。
2.1 浏览器主动发起的“安检”请求
当浏览器判定需要预检时,它会自动发起一个方法为 OPTIONS 的请求,目标 URL 和实际业务请求的 URL 完全一致。这个请求里不会携带实际业务数据,而是带着几个关键的请求头,告诉服务端“我想干什么”:
Origin: http://localhost:8080:当前页面的源,也就是我来自哪儿。- `Acce
