浅谈 AJAX 跨域请求时的 OPTIONS 方法

阅读 14947
收藏 54
2017-04-10
原文链接:wangyuhao.me

在通过 AJAX 发起 HTTP 请求的时候,我们最常用的方法大概就是 GET 和 POST 了。实际上除了这两个以外,HTTP 请求还有 PUT,DELETE,OPTIONS 等等。本文就将对 OPTIONS 请求的作用进行介绍,并解决我前两天遇到的一个与它相关的问题。

起因

上周在写一个 Vue 项目的时候要用到网络请求库,由于之前用的一直是 vue-resource,而 Vue 的作者尤雨溪几个月前发表了一篇博客,表示 vue-resource 将不再是官方推荐的库,建议大家以 axios 替代,因此我便选择了 axios。

实际上和大多数 AJAX 库类似,axios 的 API 设计非常简单,于是我草草看了下示例就开撸代码了。然而在 POST 跨域请求服务器资源的时候,控制台报了这么一个错:

XMLHttpRequest cannot load xxxxxxxx. Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.

与此同时,Chrome 的网络面板里,这个请求并不是 POST,而是 OPTIONS,一个之前见过一眼但是并不了解的方法。

问题的根源

起初我还以为是服务器跨域的设置出了问题,但我还是先去看了下 preflight response 是个什么,MDN 写道:

The Cross-Origin Resource Sharing standard works by adding new HTTP headers that allow servers to describe the set of origins that are permitted to read that information using a web browser. Additionally, for HTTP request methods that can cause side-effects on server’s data (in particular, for HTTP methods other than GET, or for POST usage with certain MIME types), the specification mandates that browsers “preflight” the request, soliciting supported methods from the server with an HTTP OPTIONS request method, and then, upon “approval” from the server, sending the actual request with the actual HTTP request method. Servers can also notify clients whether “credentials” (including Cookies and HTTP Authentication data) should be sent with requests.

简单来说,就是对于一些可能对服务器数据有影响的请求,如 PUT,DELETE 和搭配某些 MIME 类型的 POST 方法,浏览器必须先发送一个“预检请求”——也就是刚才说的 preflight response,来确认服务器是否允许该请求,允许的话再真正发送相应的请求。

这么说来,按照错误日志的说法,我的 Content-Type 不受服务器支持?不信邪的我在 jsfiddle 用 jQuery 的 $.post 请求了一下接口,这次却没有任何问题。重新检查网络面板的请求,发现 jQuery 发送的请求 Content-Type 为 application/x-www-form-urlencoded,而之前用 axios 发送的请求 Content-Type 为 application/json,看来这就是问题所在了。

在 jQuery 官方文档中写道

For cross-domain requests, setting the content type to anything other than application/x-www-form-urlencoded, multipart/form-data, or text/plain will trigger the browser to send a preflight OPTIONS request to the server.

也就是说,发送的请求内容类型如果不是 application/x-www-form-urlencoded,multipart/form-data 或 text/plain 这三者的话,便会触发 OPTIONS 请求,而 jQuery 发送的请求内容类型默认值为 application/x-www-form-urlencoded,这就是 jQuery 可以顺利请求的原因。

还是在 MDN 里,提到了非简单请求必须先发送预检请求这一点,而以下条件之外的请求都算非简单请求:

In particular, a request is preflighted if any of the following conditions is true:

  • If the request uses any of the following methods:
    PUT
    DELETE
    CONNECT
    OPTIONS
    TRACE
    PATCH

  • Or if, apart from the headers set automatically by the user agent (for example, Connection, User-Agent, or any of the other header with a name defined in the Fetch spec as a “forbidden header name”), the request includes any headers other than those which the Fetch spec defines as being a “CORS-safelisted request-header”, which are the following:
    Accept
    Accept-Language
    Content-Language
    Content-Type (but note the additional requirements below)
    DPR
    Downlink
    Save-Data
    Viewport-Width
    Width

  • Or if the Content-Type header has a value other than the following:
    application/x-www-form-urlencoded
    multipart/form-data
    text/plain

注意第三条,和刚刚 jQuery 文档的说法一致。

解决办法

这下我们知道问题出在哪里了。要想避免这个问题出现,要么在服务器端进行设置,要么让请求避开上面的限制。

在 vue-resource 中,我们可以设置 Vue.http.options.emulateJSON = true

而在 axios 中,可以使用 URLSearchParams API

var params = new URLSearchParams();
params.append('param1', 'value1');
params.append('param2', 'value2');
axios.post('/foo', params);

或者 qs

var qs = require('qs');
axios.post('/foo', qs.stringify({ 'bar': 123 }));

参考

  1. HTTP access control (CORS)

  2. HTTP OPTIONS

  3. jQuery.ajax

  4. vue-resource

  5. axios

评论