HTTP GET 请求能不能带 body

2021-10-27 ⏳4.9分钟(2.0千字)

最近在知乎上聊了一下对 REST 规范的看法,其中涉及到 http 的 get 请求到底能不能带 body 的问题。我的看法招来了刘正网友的批判。知乎的回复最长只能一千字,所以单独写一篇文章来回应。

刘正的评论原文如下:

本着不要教坏小朋友的选择,指出其中几个比较搞的地方。 首先 REST 是标准,不是实现,每一个语言,每一套框架它的落地是不一样的,所以get请求能够上传body这句话是正儿八经的鬼扯,至少jdk servlet 2的实现就不行。 其次,为什么建议读写方法分离。原因在于安全,这不是我说的,参考亚马逊S3 https 鉴权规范第二版,明确的提出只要是post提交,就一定存在注入风险,原则上只要有提交,注入不可避免,所以亚马逊S3针对post会专门的进行提交隔离审计。 你的项目运行三年多没出什么问题,至少说明你的项目没有进行夸组织,跨平台的大规模系统集成。 仔细看看 apache 开源也好,google cloud 组件也罢,全部严格遵循读写分离方法规范。你特例独行,不是不可以,但只要是技术,一定将就成本问题,人为的构造了通讯壁垒,最后的结果就是返工。 这是标准,标准的作用是降低使用标准的人进入的门槛,get/put/post/delete,queryparam,pathparam,formparam 这些请求方式不是你定的,不是我定的,也不是一家公司,一个技术强人能改变的,是整个生态数千家企业共同遵循的行业标准 说句不好听的话。硬怼的最终结果就是成本成倍增加。然后发现搞不拢留下一堆神仙代码然后跑路。留给接盘侠一坨难以言喻的大坑,然后推倒重来。

感谢他不吝赐教,下面我一一回应。

关于 GET 请求能不能传 body 的问题,我的原话是

(有人可能会说 GET 不能附加 body,其实这是不对的。GET 请求也可以像 POST 那样提交数据。)。

先明确一点,这是一个 HTTP 协议相关的问题,而不是 REST 相关的问题。REST 只是复用了 HTTP 协议的语言规范,但 REST 不能等同于 HTTP,HTTP 更不能等同于 REST。所以,我个人对于 GET 请求能否加 body 这个 HTTP 协议问题的认知,也不应该被当成考察我个人对 REST 认知的依据。这是两码事。 再回来说 GET 请求的 body 问题。请参考 rfc7231 的 4.3.1 小节:

A payload within a GET request message has no defined semantics; sending a payload body on a GET request might cause some existing implementations to reject the request.

协议并没有禁止 GET 请求传 body,只是说这种行为是 no defined semantics。

那现实中能不能传呢?能传。为了不误人子弟,我专门测试了 Go 语言官方 http 服务,支持传 body。

package main

import (
        "fmt"
        "io"
        "net/http"
)

func hello(w http.ResponseWriter, req *http.Request) {
        b, _ := io.ReadAll(req.Body)
        w.Write(b)
}

func main() {
        http.HandleFunc("/hello", hello)
        http.ListenAndServe(":8081", nil)
}

代码的功能是把请求的 body 作为响应内容发回调用方。可以使用 curl 测试:

curl -X GET 127.0.0.1:8081/hello -d '{"a":1}'

curl 的 -X 可以指定请求用的方法,-d可以指定请求的 body 内容。大家可以自己测试一下,看看是什么效果。

大家可能觉得 Go 语言用得没那么广泛。于是我又用 Nginx 为 Go 语言的测试代码做代理,同样也支持在 GET 请求里加 body。Nginx 的核心配置如下:

http {
  server {
    listen 8080;
    location / {
      proxy_pass http://127.0.0.1:8081
    }
  }
}

然后用 curl 换个端口测试,结果是一样的。

那现实中有没有比较有名的项目使用GET方法传参数呢?有!那就是ElasticSearch。ES 的搜索接口从 REST 风格角度看应该用 GET 请求。但是完整的查询 DSL 是 json 格式,所以支持通过 body 提交查询数据。对应的官方文档在这里Search API | Elasticsearch Guide 7.15 Elastic

到这里我只能得出结论,所谓的「所以get请求能够上传body这句话是正儿八经的鬼扯」才是正儿八经的鬼扯。但无论如何,这个问题跟REST没有太大的关系。

刘正提到的「为什么建议读写方法分离」,我很赞同。我们要尽量做到读写分离。但这好像跟用不用 GET/POST 没有关系。只用 POST 就不能读写分开了吗?读写分离是针对具体的接口而言的。POST /GetBook 是读查询,POST /DelBook 是删除,没看出来这有什么问题。一提到读请求就一定要用 GET,这个就有点牵强。POST 接口也可以只读,POST 接口也可以幂等。我在回答中提到过 grpc,基于 http2,但用的全是 post 请求。你不能说 grpc 接口不支持只读和幂等吧。

读写分离的第二个问题是项目毒化。我们在项目刚上线的时候还有可能确保读写分离。但随着系统规模的增长、业务的发展和人员的变更,保不齐就需要在读接口中加入部分写逻辑。可能是我才疏学浅,至不我是不能打包票说所有的接口都是读写分离的。而且我也没见到过那个系统能完全做到这一点。

读写分离,应该说还是比较粗粒度的一种分类方式。如果真是要追求案例,那应该引入更复杂的权限系统,比如 RABC 什么的。这种权限系统需要在接口文档中加以详细的描述,而不是简单地用 GET/POST 来区分读写接口就能实现的。

总结一来有两点。第一点,复杂的系统在长期演化中很难保持全部读写分离。第二点,全部使用 POST 方法并不影响接口的读写分离,更不是要求大家放弃读写分离的原则。

刘正提到的 aws s3 是一类非常简单而且功能非常明确的系统。说白了就是文件的增删改查。这类场景是完美匹配REST风格的。只有一种资源,叫文件。只有明确的几种操作。如果不用REST而自己设计新的接口,那跟REST风格接口也不会有太大的差异。但现实中的业务系统比较一个 s3 的接口不知道要复杂多少倍(我说的是接口,s3 的内部实现可能很复杂,但对外提供的接口却很简单)。

刘正还提到了 POST 请求的注入问题。我还请他能给出具体的例子或者官方链接。他提出的「亚马逊S3针对post会专门的进行提交隔离审计」跟用不用 GET 也关系不大。

至于他说的其他内容,则完全是脱离事实依据的臆想,我就不一一反驳了。说到底还是认知问题,没有必要。