简单请求为何无需预检

理解 CORS 中简单请求跳过预检的原因

问题

在 CORS 机制中,为什么简单请求不需要发送预检请求(OPTIONS),而复杂请求需要?

解答

什么是简单请求

满足以下所有条件的请求被视为简单请求:

  1. 方法限制:GET、HEAD、POST 之一
  2. 请求头限制:只能包含以下头部
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(仅限下面三种)
  3. Content-Type 限制
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

不需要预检的原因

核心原因:历史兼容性

在 CORS 出现之前,浏览器已经允许通过 <form> 表单和 <img> 标签发起跨域请求:

<!-- 这些跨域请求在 CORS 之前就被允许 -->
<form action="https://other-site.com/api" method="POST">
  <input type="text" name="data">
  <button type="submit">提交</button>
</form>

<img src="https://other-site.com/image.png">

这些传统方式能发起的请求,恰好就是”简单请求”的范围。服务器早已能处理这类请求,不会因为它们产生意外的副作用。

预检的目的是保护服务器

// 复杂请求示例 - 需要预检
fetch('https://api.example.com/data', {
  method: 'DELETE',  // 非简单方法
  headers: {
    'Content-Type': 'application/json',  // 非简单 Content-Type
    'X-Custom-Header': 'value'  // 自定义头
  }
});

预检请求让服务器有机会在实际请求到达前决定是否接受。对于可能修改服务器数据的复杂请求,这层保护很重要。

请求流程对比

// 简单请求 - 直接发送
fetch('https://api.example.com/users', {
  method: 'GET'
});
// 浏览器直接发送 GET 请求,检查响应头决定是否暴露给 JS

// 复杂请求 - 先预检
fetch('https://api.example.com/users', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'test' })
});
// 浏览器先发 OPTIONS 请求
// 服务器返回允许的方法和头部
// 预检通过后才发送实际 PUT 请求

预检请求示例

# 预检请求
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: https://mysite.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type

# 预检响应
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://mysite.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400

关键点

  • 简单请求的范围等同于 CORS 之前浏览器就能发起的跨域请求
  • 预检机制是为了保护服务器免受新型请求的意外影响
  • 简单请求仍会检查响应头,只是跳过了预检步骤
  • Access-Control-Max-Age 可缓存预检结果,减少 OPTIONS 请求次数
  • 即使是简单请求,服务器仍需返回正确的 CORS 头部,否则响应不会暴露给 JavaScript