HTTP GET 请求可以有 body 吗?
本文有点啰嗦,没耐心的直接拉到末尾看 结论 。
在网上可以经常看到关于 HTTP GET 请求能不能带 body 的讨论。有的人认为 GET 请求可以带 body,有的认为 GET 请求不能带 body,还有些人认为可以带但最好不带。大家各执己见,谁都没有 100% 说服谁。
我个人在工作中从来没有见哪个同事用 HTTP GET 请求的 body 携带数据。写了两年爬虫,经常抓包分析目标网站的请求数据,也没见到哪个网站的 GET 请求携带 body。看 HTTP 协议的时候也没特别注意关于这方面的规定。所以我一直默认为不会有人把数据放在 HTTP GET 请求的 body 里,但不知道 GET 携带 body 是否符合规范。今天专门梳理一下。
HTTP 协议对 GET 请求 body 的规定
RFC 1945
RFC 1945 发布于 1996 年,描述了 HTTP/1.0 。
其中和 body 有关的第 7 节提到了一下内容:
Full-Request and Full-Response messages may transfer an entity within some requests and responses. An entity consists of Entity-Header fields and (usually) an Entity-Body.
译:Full-Request 和 Full-Response 消息可以在某些请求和响应中传输实体。实体包括实体首部字段,并且通常包括一个实体 body。
说明 HTTP/1.0 会通过请求或响应的 body 传输实体,并且没有限定哪些请求方法不能传输实体。也就是说 GET 也可以有 body。
另外其 8.1 节关于 GET 和 POST 方法有如下描述:
The GET method means retrieve whatever information (in the form of an entity) is identified by the Request-URI.
译:GET 方法表示查询由 Request-URI 标识的任何信息(形式为实体)。
The POST method is used to request that the destination server accept the entity enclosed in the request ...
译:POST 方法用于请求目标服务器接受包含在请求中的实体 ...
说明 GET 方法的语义是请求实体,POST 方法的语义是提交实体,两者有明确的分工。
RFC 1945 这两处的内容可以提炼出两条信息:
- 请求或响应需要传输实体时才会有 body。
- GET 请求用于请求实体,而不是传输实体。
根据这两条信息可以推出,GET 请求没有传输实体的语义,自然也不需要 body。但 RFC 1945 也没有明确规定 GET 请求不能传输实体、不能有 body。所以按 HTML 1.0 规范,GET 请求是可以有 body 的,只不过没有为其定义语义。
RFC 2068
RFC 2068 发布于 1997 年,描述了 HTTP/1.1。
RFC 2068 是对 RFC 1945 的更新,在 4.3 节有以下描述:
A message-body MAY be included in a request only when the request method allows an entity-body.
译:只有当请求方法允许使用实体 body 时,请求中才可以包含消息 body。
第 9 节是关于各个 HTTP 请求方法的描述,但只有第 9.8 节提到:
A TRACE request MUST NOT include an entity.
译:TRACE 请求必须不能包含实体。
另外,对 PUT、POST 的描述都默认有实体。但是对 GET 描述并没有提到请求中是否能包含实体 body。
也就是说 TRACE 不允许包含 body,PUT、POST 请求包含 body,但是 GET 没有明确说明,这种没说明的情况到底是允许还是不允许呢?
RFC 2616
RFC 2616 发布于 1999 年,是对 RFC 2068 的更新,还是描述的 HTTP/1.1。
其 4.3 节增加了如下描述:
A message-body MUST NOT be included in a request if the specification of the request method does not allow sending an entity-body in requests. A server SHOULD read and forward a message-body on any request; if the request method does not include defined semantics for an entity-body, then the message-body SHOULD be ignored when handling the request.
译:如果本规范规定了某个请求方法不允许发送实体,则绝不能在请求中包含消息 body。服务器应该读取和转发任何请求的消息体(body);如果某个请求方法没有定义实体语义,那么在处理请求时应该忽略消息体(body)。
但第 9 节对于各个方法的描述中还是没有说 GET 请求是否能有 body。
RFC 7231
2004 年发布的 RFC 7230~7235 是对 RFC 2626 的修订。其中 RFC 7231 是 HTTP 的“核心”语义规范,终于在 4.3.1 节明确提到了 GET 请求的 body:
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)没有定义的语义;在 GET 请求上发送有效负载主体可能会导致某些现有实现拒绝该请求。
说明修订规范的人也知道有的 HTTP server 实现会拒绝带有 body 的 HTTP GET 请求。但是为什么规范迭代了这么多次都不规定每个请求方法是否能包含 body 呢?留下这么大的争议空间也是醉了。
现有实现对 GET 请求 body 的支持
浏览器中的 GET 请求
XMLHttpRequest
在 XMLHttpRequest 规范 中有如下描述:
-
If
this
’s
request method
is
GET
orHEAD
, then set body to null.
实际在 chrome 浏览器中测试时,用 XMLHttpRequest 发送 GET 请求并带 body,body 参数会被忽略。
说明从规范到实际,XMLHttpRequest 都不支持 GET 请求 body。
Fetch
在 chrome 浏览器中测试时,Fetch 发送带 body 的 GET 请求会报错:
Uncaught (in promise) TypeError: Failed to execute 'fetch' on 'Window': Request with GET/HEAD method cannot have body.
HTTP 请求库
测试用常用的 HTTP 请求库发出带 body 的请求,并抓包验证。
Requests
Requests 是 Python 最流行的 HTTP 请求库。
import requests
requests.get(
url="http://127.0.0.1:8080",
proxies={"http": "http://127.0.0.1:8888", "https": "https://127.0.0.1:8888"},
data={"name": "x"},
)
抓包的结果如下:
GET http://127.0.0.1:8080/ HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: python-requests/2.27.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 6
Content-Type: application/x-www-form-urlencoded
name=x
说明 Requests 支持 GET 请求 body。
HTTPX
HTTPX 是 Python 正在崛起的 HTTP 请求库。
HTTPX 的便捷方法
httpx.get
并没有提供填充 body 的参数,只能用高级方法
httpx.request
。如下:
import httpx
rep = httpx.request(
method="GET",
url="http://127.0.0.1:8080",
json={"name": "x"},
proxies={"http://": "http://127.0.0.1:8888", "https://": "https://127.0.0.1:8888"},
)
通过 fiddler 抓包到的请求数据如下:
GET http://127.0.0.1:8080/ HTTP/1.1
Host: 127.0.0.1:8080
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
User-Agent: python-httpx/0.21.3
Content-Length: 13
Content-Type: application/json
{"name": "x"}
说明 HTTPX 支持 GET 请求 body。
OkHttp
todo...
axios
axios 是一个基于 Promise 的 HTTP Client 库,可用于浏览器和 Node.js。也是非常流行,在 JavaScript 技术栈中的地位类似 Requests 在 Python 世界的地位。
在 Node.js 中用 axios 发出一个带 body 的 GET 请求:
import axios from "axios";
await axios.get("https:/baidu.com", {
data: { name: "x" },
proxy: { host: "127.0.0.1", port: 8888 },
});
抓包发出的请求:
GET https://null/baidu.com HTTP/1.1
Accept: application/json, text/plain, */*
Content-Type: application/json
User-Agent: axios/0.24.0
Content-Length: 12
host: null
Connection: close
{"name":"x"}
说明 Node.js 中的 axios 支持 GET 请求 body。
superagent
一款小型渐进式客户端 HTTP 请求库,支持许多高级 HTTP 客户端功能。
import superagent from "superagent";
import superagentProxy from "superagent-proxy";
superagentProxy(superagent);
superagent
.get("http://127.0.0.1")
.proxy("http://127.0.0.1:8888")
.send({ name: "x" })
.end();
抓包结果:
GET http://127.0.0.1/ HTTP/1.1
Host: 127.0.0.1
Accept-Encoding: gzip, deflate
Content-Type: application/json
Content-Length: 12
Connection: close
{"name":"x"}
node-fetch
node-fetch 是 Node.js 的一个 HTTP 请求库,和浏览器的
window.fetch
保持一致。尝试用 node-fetch 3.1.0 发送带 body 的 GET 请求:
import fetch from "node-fetch";
import { env } from "process";
env["http_proxy"] = "http://127.0.0.1:8888";
env["https_proxy"] = "https://127.0.0.1:8888";
const response = await fetch("http://127.0.0.1:8080", {
method: "GET",
body: JSON.stringify({ name: "x" }),
await response.json();
结果报错如下:
TypeError: Request with GET/HEAD method cannot have body
看来 node-fetch 明确不支持 GET 请求 body,和浏览器的
window.fetch
保持一致。
node.js 内置模块 http
import http from "http";
const req = http.request({
method: "GET",
path: "https://baidu.com",
host: "127.0.0.1",
port: 8888,
let data = JSON.stringify({ name: "x" });
req.setHeader("Content-Length", data.length);
req.end(data);
抓包结果:
GET https://baidu.com HTTP/1.1
Host: baidu.com
Content-Length: 12
Connection: close
{"name":"x"}
说明 node 的内置 http 模块也是支持发送 GET 请求 body 的。
常用 HTTP 工具
curl
curl 可能是最常用的 http 命令行工具了。
$ export http_proxy=http://127.0.0.1:8888
$ export https_proxy=http://127.0.0.1:8888
$ curl -X GET -d "name=x" http://baidu.com
抓包结果:
GET http://baidu.com/ HTTP/1.1
Host: baidu.com
User-Agent: curl/7.79.1
Accept: */*
Connection: Keep-Alive
Content-Length: 6
Content-Type: application/x-www-form-urlencoded
name=x
说明 curl 也是支持 GET 请求 body 的。
Postman
经过测试,可以。
Apifox
经过测试,可以。
Fiddler
Fiddler 是常用的 HTTP 抓包工具,也可以用来发送 HTTP 请求。发个带 body 的 GET 请求试试。
可以看出 Fiddler 是可以发出带 body 的 GET 请求的,但是会用红色表示警告。
nginx
nginx 非常强度,通常用作 HTTP server 或反向代理 server。
经过测试 nginx 也支持带 body 的 GET 请求。
Web Server 框架
Flask
Flask 是 Python 社区最流行的 Web Server 框架。用 Flask 写一个 HTTP Api,看看能不能处理 GET 请求的 body。
代码如下:
from flask import Flask, request
app = Flask(__name__)
@app.get("/")
def get():
print(request.form["name"])
return f"you name is {request.form['name']}"
app.run(host="127.0.0.1", port=8080)
该应用接收一个 GET 请求并解析 body 中的表单参数。
启动后,另起一个终端,用 curl 发出带 body 的 GET 请求:
$ curl -X GET -d "name=x" http://127.0.0.1:8080/
you name is x
可以看出 Flask 确实正确处理了本次 GET 请求的 body。
FastAPI
FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架。已经遮住了 Flask 的光芒。
用 FastAPI 写一个 HTTP Api,看看能不能处理 GET 请求的 body。代码如下:
// main.py
from fastapi import Body, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
@app.get("/")
async def update_item(item: Item):
results = {"name": item.name}