CORS 预检请求(OPTIONS)到底是什么?前端开发者必知的跨域机制
你在浏览器里调一个 API,明明 URL 没问题、参数也对,控制台却甩出一段红色报错:
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: Response to preflight request doesn't pass
access control check: No 'Access-Control-Allow-Origin' header is present.
更让人困惑的是,打开 Network 面板一看,请求列表里多了一个你从未主动发起的 OPTIONS 请求。这个 OPTIONS 就是 CORS 预检(Preflight)。本文把「简单请求 vs 预检」、Allow-* 头、凭据模式和常见踩坑拆开讲;需要对照真实响应时,可打开站内 API 测试工具(注意:浏览器直连仍受 CORS 约束,服务端代理才绕过浏览器限制)和 HTTP 状态码速查。
同源、CORS、预检:三层概念别混
浏览器的同源策略规定:网页默认只能读写「同协议 + 同主机 + 同端口」的响应。CORS(Cross-Origin Resource Sharing)是服务端用响应头告诉浏览器:「这个跨源请求可以读给前端脚本」。
预检不是「多发一次业务请求」,而是浏览器在发送非简单请求之前,先发一个 OPTIONS,问服务端是否同意本次方法与头。预检失败时,实际业务请求根本不会发出——所以你会在 Network 里只看到红色 OPTIONS,而业务路径上没有 POST/PUT。
相关排查故事(Token、401、405 与预检叠在一起时)见 接口测试怎么带 Token、过 CORS。
简单请求 vs 预检:触发条件要抠细节
规范用语是 CORS-safelisted method / header。同时满足下列条件才是「简单请求」,浏览器不会先发预检:
| 维度 | 简单请求要求 |
|---|---|
| 方法 | 仅 GET、HEAD、POST |
| 请求头 | 仅安全列表内的头(如 Accept、Accept-Language、Content-Language,以及受限的 Content-Type) |
Content-Type(若有) | 仅 text/plain、multipart/form-data、application/x-www-form-urlencoded |
任意一条打破就会触发预检,前端最常见的几条:
- 方法是
PUT/DELETE/PATCH(或其它非安全列表方法) - 带了自定义头:
Authorization、X-API-Key、X-Requested-With等 Content-Type: application/json(JSON API 几乎都会踩)- 使用了非安全列表头,或对安全列表头写了「浏览器认为不简单」的取值(少见但存在)
// 会触发预检:JSON Content-Type
fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'test' }),
});
// 会触发预检:非简单方法
fetch('https://api.example.com/users/1', { method: 'DELETE' });
// 会触发预检:自定义头
fetch('https://api.example.com/data', {
headers: { 'X-API-Key': 'abc123' },
});
对照记忆:表单时代的「传统 POST」往往是简单请求;现代 fetch + JSON + Bearer Token 几乎总是预检。
预检请求发了什么?服务端要回什么?
浏览器自动发出的 OPTIONS 大致如下:
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
| 请求头 | 含义 |
|---|---|
Origin | 页面源(协议+主机+端口) |
Access-Control-Request-Method | 即将发起的业务方法 |
Access-Control-Request-Headers | 即将带上的非简单头(逗号分隔,大小写不敏感) |
服务端「批准」时常见响应(状态码多为 204 或 200,有无 body 均可;含义见 HTTP 状态码指南):
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Vary: Origin
Access-Control-Allow-* 对照表
| 响应头 | 作用 | 常见踩坑 |
|---|---|---|
Access-Control-Allow-Origin | 允许哪个源读响应。可为具体源或 *(见凭据节) | 漏配;或写死了生产域名却在 localhost 调试 |
Access-Control-Allow-Methods | 允许的方法列表 | 只写了 GET, POST,业务却用 PUT/DELETE |
Access-Control-Allow-Headers | 允许的请求头 | 忘了放行 Authorization / Content-Type |
Access-Control-Max-Age | 预检结果缓存秒数 | 配错后浏览器仍缓存旧结果,改完服务端「好像没生效」 |
Access-Control-Allow-Credentials | 是否允许携带 Cookie / 认证信息 | 与 Allow-Origin: * 互斥 |
Access-Control-Expose-Headers | 哪些响应头可被前端 getResponseHeader 读到 | 只配了 Allow,前端仍读不到自定义响应头 |
预检阶段浏览器主要核对:Allow-Origin 是否覆盖当前 Origin;Allow-Methods 是否包含 Request-Method;Allow-Headers 是否覆盖每一个 Request-Headers。任一不匹配,业务请求不会发出。
业务响应(预检通过后的真正 GET/POST…)同样需要正确的 Access-Control-Allow-Origin(以及凭据场景下的 Allow-Credentials)。只修好 OPTIONS、忘了给实际响应加 CORS 头,仍会在「能发出去却读不到 body」处失败。
凭据模式:Cookie 与 * 不能共存
当前端使用:
fetch(url, { credentials: 'include' });
// 或 XHR: xhr.withCredentials = true
浏览器会带上 Cookie(及部分认证态)。此时 CORS 规则收紧:
- 服务端必须返回
Access-Control-Allow-Credentials: true Access-Control-Allow-Origin不能是*,必须是精确回显的具体源(例如https://app.example.com)- 预检与业务响应都要满足上述两点
典型报错文案:The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'。
实践建议:按请求的 Origin 做白名单匹配后再回写,并加上 Vary: Origin,避免 CDN/代理把「给 A 源的响应」错误缓存给 B 源。
常见踩坑清单(按出现频率)
1. 预检 OPTIONS 返回 405 / 404
框架路由只注册了 POST /users,没处理 OPTIONS /users。浏览器先撞上 405 Method Not Allowed,预检失败。状态含义可查 HTTP 状态码工具。修复:在网关或应用层对跨域路径显式处理 OPTIONS(或使用成熟 CORS 中间件)。
2. application/json 误以为「只是 POST」
POST 本身可以是简单请求,但换上 JSON 的 Content-Type 就不是。本地用 curl/Postman 通、浏览器失败,经常就是这一层。
3. 只配了 Allow-Origin,没配 Allow-Headers / Methods
预检带着 Authorization 来,响应却没列出该头 → Request header field Authorization is not allowed by Access-Control-Allow-Headers。
4. 网关剥掉了 CORS 头
应用层加了头,前面的 Nginx / CDN / API Gateway 对 OPTIONS 直接短路或覆盖响应,浏览器仍看不到 Allow-Origin。要在浏览器实际收到的响应头上核对,而不是只看应用日志。
5. Max-Age 缓存了错误的预检结果
调试时改了 CORS 配置,浏览器仍用缓存的预检答案。可临时关缓存、换无痕窗口,或把 Max-Age 在开发环境调短。
6. 重定向与跨源
跨源请求若 302 到另一主机,CORS 检查会对重定向链路更严格;有些环境下「看起来像网络错误」。调试时先确认最终 URL 与中间跳转(API 测试工具 可观察状态码与跳转行为)。
7. 把「混合内容」当成 CORS
HTTPS 页面请求 http:// 目标会被浏览器在发送前拦截,与 CORS 无关。站内 API 工具在直连模式下也会区分这类失败,避免误导你去改服务端 CORS。
用本站工具怎么对照排查
- 先分清模式:API 测试工具 默认可在「浏览器直连」与「服务端代理」之间切换。
- 直连:与页面里的
fetch一样,受 CORS / 混合内容约束;适合复现前端真实失败。 - 代理:请求经本站服务端转发,不走浏览器同源策略,适合确认「接口本身是否通、业务状态码是否正常」。
- 直连:与页面里的
- 对照状态码:预检常见
204/200;失败侧常见403/404/405。含义不明时用 HTTP 状态码速查。 - 手动发 OPTIONS:在工具里选 OPTIONS,带上你关心的头(或先看业务请求会触发哪些
Access-Control-Request-*),检查响应是否包含完整的Access-Control-Allow-*。 - Token 与 401 叠在一起时:先排除预检,再查鉴权——见 带 Token 过 CORS。
生产环境仍必须正确配置 CORS:代理只是调试手段,真实用户浏览器不会走你的调试代理。
开发阶段的临时方案(权限不够改服务端时)
- 前端开发代理:Vite / webpack
devServer.proxy把/api转到同源,浏览器不再跨源。 - 本站服务端代理:在 API 测试工具 开启代理,验证接口逻辑。
- 浏览器插件禁用 CORS:仅限本机开发,勿用于日常浏览。
// Vite 代理示例
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
};
一句话收口
非简单请求 → 浏览器先 OPTIONS 预检 → 服务端用 Access-Control-Allow-* 批准方法与头 → 再发业务请求;业务响应也要带齐 CORS 头。排查时打开 Network,对齐 Access-Control-Request-* 与 Access-Control-Allow-*;想先确认接口是否通,用 API 测试工具 的代理模式;想复现页面失败,用直连模式。状态码含义随时查 HTTP 状态码。
实现注记
站内 API 测试工具 在直连失败且像网络层拦截时,会弹出 CORS 提示,并提供「开启服务端代理并重试」。代理路径走同源的 /api/proxy,由服务端代发,因此浏览器不再对目标站做预检——这与「curl / Postman 不受 CORS 约束」同类,但不能当成生产前端的解决方案。旧版本文曾写「工具完全不受 CORS 限制」,容易与直连模式矛盾;现以「直连受约束 / 代理绕过」为准。AdSense 冻结期内不新增工具 slug,预检相关能力继续落在既有 api-tester 与 http-status-codes 上。
相关阅读
ToolVault 工具匣 出品的开发者指南。相关工具:API 测试、HTTP 状态码速查、HTTP 方法速查、curl 转代码。访问 首页 查看更多开发者工具。
相关工具
相关文章
HTTPS 证书链不完整:浏览器不信任但解码「看起来正常」
叶证书日期有效、SAN 也对,浏览器仍报不受信任?多数是缺中间证书或 fullchain 顺序错。用 openssl s_client 与本地证书解码器逐级核对 Issuer→Subject。
HTTP API 测试完全指南:从基础到高级调试技巧
掌握 HTTP API 测试的核心技巧,包括 GET/POST/PUT/DELETE 请求、请求头配置、请求体格式、认证方式和调试方法。使用在线 API 测试工具提升开发效率。
HTTP GET vs POST:什么时候用哪个?语义、安全与最佳实践
GET 和 POST 的语义差异、幂等性、缓存、URL 长度限制、安全考量、RESTful 约定、常见误用与 curl 示例。附决策表,帮你正确选择 HTTP 方法。