浏览器仅对非简单请求自动触发OPTIONS预检:方法为PUT/DELETE等、含Authorization等自定义头、Content-Type非text/plain等三类之一时发生;预检携带Origin、Access-Control-Request-Method等字段,服务器须响应合法CORS头才放行真实请求。

fetch 的“二次握手”并不是标准术语,而是开发者对预检请求(Preflight)行为的形象说法:浏览器在发真正请求前,先发一个 OPTIONS 请求探路——就像先敲门再进门。这个机制完全由浏览器自动触发,前端无法跳过或手动发起,核心目的是在执行可能有副作用的操作前,向服务器确认权限。
什么情况下 fetch 会触发预检
不是所有 fetch 都会走二次握手。只有当请求属于“非简单请求”时,浏览器才自动加一层 OPTIONS 预检:
- 使用了非安全方法:PUT、DELETE、PATCH、TRACE、CONNECT、OPTIONS
- 设置了非简单请求头:比如 Authorization、X-Requested-With,或 Content-Type: application/json
- Content-Type 值不是以下三者之一:text/plain、multipart/form-data、application/x-www-form-urlencoded
例如:fetch('/api/user', { method: 'PUT', headers: { 'Content-Type': 'application/json' } }) 一定会触发预检;而 fetch('/api/list', { method: 'GET' }) 或 fetch('/form', { method: 'POST', body: new FormData(...) }) 则不会。
预检请求里到底传了什么
浏览器发出的 OPTIONS 请求不是空跑,它会携带关键元信息,帮服务器做决策:
-
Origin:当前页面的源(协议+域名+端口),如
https://admin.example.com -
Access-Control-Request-Method:即将发送的真实请求方法,如
PUT -
Access-Control-Request-Headers:真实请求中将携带的自定义头,如
Authorization, X-App-Version
服务器收到后,要根据这些字段判断是否放行,而不是只看 URL 或固定规则。
后端响应必须包含哪些头才能通过预检
预检能否成功,取决于服务器是否在 OPTIONS 响应中返回合法的 CORS 允许头:
-
Access-Control-Allow-Origin:必须精确匹配 Origin 或设为
*(注意:带凭据时不能用*) -
Access-Control-Allow-Methods:列出允许的方法,如
GET, POST, PUT, DELETE,需包含预检中声明的Access-Control-Request-Method -
Access-Control-Allow-Headers:列出允许的请求头,需覆盖
Access-Control-Request-Headers中的所有项 - (可选但推荐)Access-Control-Max-Age:缓存预检结果,比如设为
86400表示一天内相同请求不再重复预检
漏掉任一必要头,预检就失败,fetch 就卡住,控制台报错:“No 'Access-Control-Allow-Origin' header is present…”
为什么说这是安全优先的设计
预检本质是防御性设计。比如一个恶意网站诱导用户点击,悄悄用 fetch('/api/transfer', { method: 'POST', headers: { Authorization: 'Bearer xxx' } }) 转账——若没预检,请求已抵达服务器并执行操作,仅因响应被浏览器拦截而“看似失败”,实际危害已发生。预检把校验前置到动作执行前,确保服务器明确同意该类请求才放行真实调用。

















