跳到主要内容
API 测试2026-06-16Begin7 分钟阅读

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 规则收紧:

  1. 服务端必须返回 Access-Control-Allow-Credentials: true
  2. Access-Control-Allow-Origin 不能是 *,必须是精确回显的具体源(例如 https://app.example.com)
  3. 预检与业务响应都要满足上述两点

典型报错文案: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。

用本站工具怎么对照排查

  1. 先分清模式:API 测试工具 默认可在「浏览器直连」与「服务端代理」之间切换。
    • 直连:与页面里的 fetch 一样,受 CORS / 混合内容约束;适合复现前端真实失败。
    • 代理:请求经本站服务端转发,不走浏览器同源策略,适合确认「接口本身是否通、业务状态码是否正常」。
  2. 对照状态码:预检常见 204/200;失败侧常见 403/404/405。含义不明时用 HTTP 状态码速查。
  3. 手动发 OPTIONS:在工具里选 OPTIONS,带上你关心的头(或先看业务请求会触发哪些 Access-Control-Request-*),检查响应是否包含完整的 Access-Control-Allow-*。
  4. Token 与 401 叠在一起时:先排除预检,再查鉴权——见 带 Token 过 CORS。

生产环境仍必须正确配置 CORS:代理只是调试手段,真实用户浏览器不会走你的调试代理。

开发阶段的临时方案(权限不够改服务端时)

  1. 前端开发代理:Vite / webpack devServer.proxy 把 /api 转到同源,浏览器不再跨源。
  2. 本站服务端代理:在 API 测试工具 开启代理,验证接口逻辑。
  3. 浏览器插件禁用 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 转代码。访问 首页 查看更多开发者工具。