1. 简介

在网页中通过 JavaScript 发起 HTTP 请求时,有时会在实际请求之前多出一个 OPTIONS 请求。这个行为常常让人疑惑:为什么会有这样一个“多余”的请求?它到底起到了什么作用?

本文将解释 OPTIONS 请求出现的原因,以及它是如何与 CORS(跨域资源共享) 机制相关联的。同时我们也会探讨如何避免或减少这种被称为 preflight request(预检请求) 的请求。

2. 背景知识

浏览器在发送某些跨域请求之前,会先发送一个 OPTIONS 请求,这就是所谓的 预检请求(preflight request)。它属于 CORS(Cross-Origin Resource Sharing) 机制的一部分。

CORS 是一种浏览器安全机制,用于控制跨域请求的访问权限。当网页试图访问与当前域名、协议或端口不同的服务器时,就会触发 CORS 检查。

预检请求的作用是让浏览器在真正发送请求前,先询问服务器是否允许当前请求。只有服务器明确允许,浏览器才会继续发送实际请求。

举个例子:假设网页在 foo.baeldung.com,要向 www.baeldung.com/demo 发送一个 PUT 请求,那么浏览器会先发送如下 OPTIONS 请求:

OPTIONS /demo HTTP/1.1
Host: www.baeldung.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
  AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Accept: */*
Accept-language: nl-NL,nl;q=0.9,en-US;q=0.8,en;q=0.7
Connection: keep-alive
Origin: http://foo.baeldung.com
Access-Control-Request-Method: PUT

⚠️ 即使某些请求不需要预检,服务器也必须返回 Access-Control-Allow-Origin 头,否则浏览器会拦截响应。

3. 请求类型分类

CORS 将请求分为两类:

  • 简单请求(Simple Request):无需预检的请求
  • 非简单请求(Preflighted Request):需要先发送 OPTIONS 预检

3.1 简单请求的条件

要成为简单请求,必须满足以下所有条件:

  • 请求方法为:GETPOSTHEAD
  • 请求头只能是以下几种:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(且值只能是以下三种之一):
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain
  • 请求中不能使用 XMLHttpRequestUpload 的事件监听器
  • 请求中不能使用 ReadableStream

3.2 非简单请求

不符合上述条件的请求,都会触发预检请求。例如:

  • 使用了 PUTDELETEPATCH 方法
  • 自定义了请求头,如 AuthorizationX-Requested-With
  • 使用了 application/jsonContent-Type
  • 使用了 fetch API 的 body

4. 如何避免发送 OPTIONS 请求

4.1 改为同源请求

首先确认是否真的需要跨域请求。比如:

  • 是否误用了不同协议(HTTP vs HTTPS)?
  • 是否域名配置错误?
  • 是否可以通过反向代理将 API 接口和网页放在同一个域名下?

推荐做法:使用反向代理(如 Nginx、Spring Cloud Gateway)将前后端接口统一在同一个域名下,从根本上避免跨域问题。

4.2 改为简单请求

如果你控制前端代码,可以尝试将请求改写为符合简单请求的格式:

  • 使用 GETPOST 方法
  • 设置 Content-Typeapplication/x-www-form-urlencoded

示例代码如下:

var request = new XMLHttpRequest();
request.open("POST", 'http://localhost:8080/with-valid-cors-header');
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
request.send("a=b&c=d");

⚠️ 如果你尝试发送 application/json 类型的 POST 请求,就会触发预检请求。

5. 缓存 OPTIONS 请求

如果你无法避免预检请求,可以通过缓存 OPTIONS 响应来减少请求次数。

5.1 使用 Access-Control-Max-Age

服务器可以在响应 OPTIONS 请求时设置以下头信息:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, PUT, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 300

其中:

  • Access-Control-Allow-Origin:允许的源
  • Access-Control-Allow-Methods:允许的请求方法
  • Access-Control-Allow-Headers:允许的请求头
  • Access-Control-Max-Age:预检结果缓存时间(单位:秒)

设置 Access-Control-Max-Age: 300 后,浏览器在 5 分钟内不会再发送 OPTIONS 请求,而是直接使用缓存的策略。

6. 总结

  • OPTIONS 请求是浏览器在跨域请求前发起的预检请求,属于 CORS 机制的一部分
  • 只有非简单请求才会触发预检
  • 避免预检的最有效方式是改为同源请求或简单请求
  • 若无法避免预检,可通过设置 Access-Control-Max-Age 缓存策略减少请求次数

小贴士

  • 发现请求前多了一个 OPTIONS?先确认是否真的需要跨域
  • 遇到 CORS 报错?优先检查响应头是否包含 Access-Control-Allow-Origin
  • 不想频繁触发 OPTIONS?记得设置 Access-Control-Max-Age 缓存时间

通过理解浏览器行为和 CORS 规范,我们可以更高效地调试接口、减少不必要的网络请求,提升用户体验。


原始标题:Why Is an OPTIONS Request Sent?