OPTIONS 请求(预检请求):在什么情况下会触发?

CORS 预检请求详解:何时触发 OPTIONS 请求?

大家好,欢迎来到今天的讲座!我是你们的技术讲师,今天我们要深入探讨一个在现代 Web 开发中非常关键但常常被误解的话题——CORS(跨源资源共享)中的预检请求(Preflight Request)。你可能已经遇到过这样的场景:前端发起一个 POST 请求到另一个域名的 API,浏览器却先发送了一个 OPTIONS 请求,然后才真正执行你的请求。这背后到底发生了什么?为什么浏览器要这么做?

我们不会讲“官方文档式的理论”,而是从真实开发者的视角出发,结合代码、逻辑和常见陷阱,带你彻底理解这个机制。


一、什么是 CORS?为什么需要它?

在 Web 安全体系中,浏览器实施了同源策略(Same-Origin Policy),即只有当请求的协议、域名、端口完全一致时,脚本才能访问响应内容。这是为了防止恶意网站通过 JavaScript 获取其他站点的数据。

但是,在实际开发中,我们经常需要让前端(比如部署在 http://localhost:3000)调用后端 API(比如部署在 https://api.example.com)。这就产生了“跨域”问题。

为了解决这个问题,W3C 标准引入了 CORS(Cross-Origin Resource Sharing) 协议。它允许服务器明确告诉浏览器:“我可以接受来自某些来源的请求”,从而绕过同源限制。

然而,CORS 并不是简单地加个头就能搞定的。它有一个“预检机制”——这就是我们今天的核心话题:OPTIONS 请求


二、什么时候会触发 OPTIONS 请求?

✅ 触发条件总结表:

条件 是否触发 OPTIONS
同源请求(协议+域名+端口相同) ❌ 不触发
异源请求 + 使用简单请求方法(GET/HEAD/POST)且无自定义头部 ❌ 不触发
异源请求 + 使用非简单请求方法(PUT, DELETE 等) ✅ 触发
异源请求 + 使用自定义 Header(如 Authorization) ✅ 触发
异源请求 + Content-Type 不是 application/x-www-form-urlencoded / multipart/form-data / text/plain ✅ 触发

🔍 注意:这里的“简单请求”是指符合以下两个条件的请求:

  1. 方法只能是 GET、HEAD 或 POST;
  2. 请求头只能包含 Accept、Accept-Language、Content-Language、Last-Event-ID 和 Content-Type(仅限 application/x-www-form-urlencoded、multipart/form-data、text/plain)。

一旦不满足这两个条件,浏览器就会自动发出一个 OPTIONS 预检请求,询问服务器是否允许这次跨域请求。


三、举个例子:触发 OPTIONS 的典型场景

假设你在前端写了一个这样的 Axios 请求:

axios.post('https://api.example.com/users', {
  name: 'Alice',
  email: '[email protected]'
}, {
  headers: {
    'Authorization': 'Bearer abc123',
    'X-Custom-Header': 'my-value'
  }
});

这个请求会触发 OPTIONS 请求!因为:

  • 它是跨域请求;
  • 使用了 Authorization 自定义 header;
  • 虽然是 POST,但因为有自定义 header,所以不再是“简单请求”。

浏览器行为流程如下:

  1. 第一步:发送 OPTIONS 请求

    OPTIONS /users HTTP/1.1
    Host: api.example.com
    Origin: http://localhost:3000
    Access-Control-Request-Method: POST
    Access-Control-Request-Headers: authorization,x-custom-header
  2. 第二步:服务器响应 OPTIONS
    如果服务器配置正确,返回:

    HTTP/1.1 200 OK
    Access-Control-Allow-Origin: http://localhost:3000
    Access-Control-Allow-Methods: POST, GET, OPTIONS
    Access-Control-Allow-Headers: authorization, x-custom-header
    Access-Control-Max-Age: 86400
  3. 第三步:浏览器再发送真正的 POST 请求

    POST /users HTTP/1.1
    Host: api.example.com
    Origin: http://localhost:3000
    Authorization: Bearer abc123
    X-Custom-Header: my-value

如果服务器没有正确处理 OPTIONS 请求,或者返回了错误的状态码(如 403),浏览器将直接终止整个请求,并报错:

Access to XMLHttpRequest at 'https://api.example.com/users' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

四、如何避免不必要的 OPTIONS 请求?

方案 1:使用简单请求(推荐)

如果你只需要发送基本数据,比如登录表单提交,可以这样改:

// 前端代码(保持简单)
fetch('/login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: 'username=admin&password=secret'
});

此时浏览器不会触发 OPTIONS 请求!

方案 2:服务端缓存 OPTIONS 响应(提高性能)

设置 Access-Control-Max-Age 可以让浏览器缓存预检结果一段时间(单位秒):

Access-Control-Max-Age: 86400  # 缓存一天

这意味着在这段时间内,即使再次发起相同类型的跨域请求,也不用再发 OPTIONS。

⚠️ 注意:一旦请求方式或 header 改变,缓存失效,必须重新预检。


五、Node.js Express 示例:正确处理 OPTIONS 请求

下面是一个完整的 Express 后端示例,展示如何优雅地支持 CORS 预检请求:

const express = require('express');
const app = express();

// 中间件:允许所有来源(生产环境建议具体化)
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');

  // 处理 OPTIONS 请求
  if (req.method === 'OPTIONS') {
    res.status(200).send();
    return;
  }

  next();
});

// 示例接口
app.post('/users', (req, res) => {
  console.log('收到用户创建请求:', req.body);
  res.json({ message: '用户已创建' });
});

app.listen(3001, () => {
  console.log('服务器运行在 http://localhost:3001');
});

📌 关键点:

  • 检查 req.method === 'OPTIONS' 是必须的;
  • 返回空响应体 + 200 状态码即可;
  • 不要忘记设置允许的方法和头字段。

否则,浏览器认为服务器拒绝了跨域请求,导致失败。


六、常见误区与踩坑指南

误区 正确做法
“我在服务器上加了 CORS 头就万事大吉了” ❌ 必须处理 OPTIONS 请求,否则无效
“我用了 axios 就不需要关心 OPTIONS” ❌ axios 不会帮你做预检,浏览器决定何时发 OPTIONS
“只要返回 200 就行了” ❌ 必须包含正确的 Access-Control-* 头部,否则浏览器仍会拦截
“我不懂为什么我的请求总是被阻止” ✅ 打开浏览器开发者工具 → Network → 查看 OPTIONS 请求状态和响应头

🐞 错误案例:未处理 OPTIONS 导致的问题

// 错误写法:只处理了 POST,忽略了 OPTIONS
app.post('/users', (req, res) => {
  res.json({ success: true });
});

// 结果:浏览器看到 OPTIONS 返回 405 Method Not Allowed

✅ 正确做法:

app.use((req, res, next) => {
  if (req.method === 'OPTIONS') {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'POST, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type');
    res.status(200).send(); // 必须显式返回
    return;
  }
  next();
});

七、高级技巧:使用 cors 中间件简化开发

为了避免手动编写复杂的 CORS 逻辑,推荐使用官方库 cors

npm install cors
const express = require('express');
const cors = require('cors');
const app = express();

// 全局启用 CORS(生产环境建议配置白名单)
app.use(cors());

// 或者更细粒度控制
app.use(cors({
  origin: ['http://localhost:3000'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

app.post('/users', (req, res) => {
  res.json({ message: '成功!' });
});

app.listen(3001);

这个中间件会自动处理 OPTIONS 请求,无需你自己写逻辑。非常适合快速开发阶段使用。


八、总结:记住三个要点

  1. 触发条件清晰:只要不是“简单请求”,浏览器就会发 OPTIONS。
  2. 服务端必须响应 OPTIONS:否则跨域请求永远失败。
  3. 合理利用缓存:设置 Access-Control-Max-Age 提升性能。

九、课后思考题(可选练习)

  1. 如果你有一个 React 应用部署在 http://localhost:3000,调用一个 Flask 后端 API(http://localhost:5000),并且请求头包含 X-API-Key,会发生什么?

    • ✅ 答案:会触发 OPTIONS 请求,除非后端明确允许该 header。
  2. 如何在 Nginx 中配置 CORS 支持?

    • ✅ 答案:

      location / {
        add_header Access-Control-Allow-Origin "*";
        add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
        add_header Access-Control-Allow-Headers "Content-Type, Authorization";
      
        if ($request_method = 'OPTIONS') {
          add_header Access-Control-Max-Age 86400;
          add_header Content-Length 0;
          add_header Content-Type text/plain;
          return 204;
        }
      }

希望这篇讲解能帮你彻底搞懂 OPTIONS 预检请求的本质。记住:这不是浏览器的“刁难”,而是保护你应用安全的重要机制。掌握它,你就离成为一名成熟的 Web 开发者更近一步!

如果你还有疑问,欢迎留言讨论 👇
祝你编码愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注