POST请求变成OPTIONS的原理及处理

12,365 阅读6分钟

最近公司布置了一个公众号网页项目,正好有段时间没用Vue来搭建项目了,想想还是从Vue做起,正好现在脚手架也到3.0了,试试锋利不锋利。然后就遇到了不少坑。

Vue搭建完该请求接口调试,自然是用官方推荐的Axios了,然后就遇到了一个小问题,正常的Post请求,莫名其妙的变为了OPTIONS请求。

HTTP请求方法

平时开发最常见最常用的HTTP请求应该是POST和GET。但是HTTP所提供的请求方法却不止一种。

  • GET: 通常用于请求服务器发送某些资源
  • HEAD: 请求资源的头部信息, 并且这些头部与 HTTP GET 方法请求时返回的一致. - 该请求方法的一个使用场景是在下载一个大文件前先获取其大小再决定是否要下载, 以此可以节约带宽资源
  • OPTIONS: 用于获取目的资源所支持的通信选项
  • POST: 发送数据给服务器
  • PUT: 用于新增资源或者使用请求中的有效负载替换目标资源的表现形式
  • DELETE: 用于删除指定的资源
  • PATCH: 用于对资源进行部分修改
  • CONNECT: HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器
  • TRACE: 回显服务器收到的请求,主要用于测试或诊断

至于这些方法具体的应用,这里就不做论述了

错误情况


在代码中明明是Post的请求,但是在谷歌控制面板中却显示的是OPTIONS,这是为什么?

而且下面还有一段报错。

问题根源

期初想着可能是跨域的问题,然后让后台加了响应头,但是问题并没解决。后来根据英文提示请求违反CORS协议,去MDN上查询了什么事CORS。

翻阅MDN关于CORS介绍后(CORS介绍链接),在功能概述中后看到这样一段话:

跨域资源共享标准(CORS:cross-origin sharing standard )新增了一组 HTTP首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。CORS请求失败会产生错误,但是为了安全,在JavaScript代码层面是无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。

意思就是对于跨域的请求,除了GET外或者搭配某些 MIME 类型的 POST 请求,都会先发送一次OPTIONS的预请求,用来检测服务端是否允许该跨域请求。所以对于不符合规范的POST请求就会被挡到外面,所以才会产生如上的报错,只发送了OPTIONS请求,而重要的POST没有发送。

既然知道问题出在哪里,那就需要让发送的请求符合CORS协议。而根据报错所给的提示 Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response. 结合控制面板上面请求头的信息,发现Access-Control-Allow-Headers的content-type不符合要求,所以才会触发CORS检测。

解决方案

既然知道了请求被改变的原因是因为触发了CORS检测,那么只要符合CORS的规范,规避检测,就可以保证成功。在查阅资料后发现有如下内容,能够规避检测。MDN原文如下(简单请求):


某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”,请注意,该术语并不属于 Fetch (其中定义了 CORS)规范。若请求满足所有下述条件,则该请求可视为“简单请求”:

  • 使用下列方法之一: GET HEAD POST
  • Fetch 规范定义了对 CORS 安全的首部字段集合,不得人为设置该集合之外的其他首部字段。该集合为:
    1. Accept
    2. Accept-Language
    3. Content-Language
    4. Content-Type (需要注意额外的限制)
    5. DPR
    6. Downlink
    7. Save-Data
    8. Viewport-Width
    9. Width
  • Content-Type 的值仅限于下列三者之一:
    1. text/plain
    2. multipart/form-data
    3. application/x-www-form-urlencoded

请注意上面要求的是满足所有条件。而结合控制面板的信息,以及报错信息,所发送的信息格式不属于Content-Type中的任意一种。所以触发了CORS检测,导致POST请求不成功。那么下面只需要使参数的格式是上面Content-Type中所要求的其中一种就可以了。

验证

而我当时所向后台传递的是一个参数未经处理的Object对象,不属于上面三种类型中的一种,所以解决方案就是对传递的参数进行处理满足要求。

  • 对参数进行字符串转换

    根绝以往的开发经验,对参数进行JSON.stringify()处理后,可以看到请求已经能走通,但是传递给后台的参数,并不是后台想要的。所以这样作可能需要后台的配合。 PS:上一家公司就是这样处理参数的,所以这里也是验证猜测,没想到确实有点用。

    可以看到参数是字符串的形式。

  • 在 axios 中,可以使用 URLSearchParams API

    var data = new URLSearchParams();
    data.append('id', '1');
    data.append('name', 'minmin');
    data.append('age', '23')
    axios.post('url, data).then(
        res => {
            ...
        }
    )
    

    但是有一点不方便,如果参数特别多的话,这种方式费事费力,所以还是用插件解决吧

  • 用插件解决,在项目中引用qs

    npm install --save  qs
    安装不上的用淘宝镜像,然后
    cnpm install --save qs 
    
    //封装请求方法,所有参数统一用qs.stringify(data)处理
    function httpRequest(url, method, data) {
        let rdata = { ...publicData, ...data }
        rdata = qs.stringify(rdata)
        if (method === "post") {
            return post(url, rdata)
        } else {
            return get(url, rdata)
        }
    }
    

可以在view source中看到参数已经被转码了。

结语

个人还是建议在项目中使用插件解决吧,每次通过append参数费时又费力,当然如果公司用的是第一种向后台传参,不用插件也没什么问题。