HTTP 里很多所谓“常识”其实混在了不同层次:

  1. HTTP 报文层:这种消息在协议格式上能不能表达?
  2. HTTP 语义层:RFC 有没有给这种写法定义标准语义?
  3. Web 平台层:浏览器的 Fetch / XMLHttpRequest 是否允许网页这样发?
  4. 中间设施层:代理、CDN、WAF、网关、缓存是否正确支持?
  5. 框架层:Nginx、Spring、Express、NestJS 等最终是否接受并按预期处理?

因此,“HTTP 能不能这么干?”往往不是一个 Yes / No 问题。

1. GET 可以在 HTTP 消息层出现 request body,但没有通用语义

很多教材会直接写:

GET 不能有 body。

这个说法过于绝对。

RFC 9110 明确指出:request message framing 与 method 无关;因此,GET 并不是因为报文格式本身而无法携带 content。问题在于,RFC 对 GET request content 没有定义通用语义,而且客户端 SHOULD NOT 随意发送 GET content,除非直接访问一个已经明确表示支持这种用法的源服务器。

所以更准确的说法是:

HTTP 报文层:        可以表达 GET + body
HTTP 通用语义:      body 没有通用定义
RFC 建议:           不应随意发送
中间件兼容性:       可能拒绝,甚至视为 request smuggling 风险

参见 RFC 9110 §9.3.1

浏览器 Fetch:直接禁止 GET / HEAD + body

fetch("/api/user", {
  method: "GET",
  body: JSON.stringify({ name: "asuhe" })
});

按照 WHATWG Fetch Standard,构造 Request 时只要 method 是 GETHEAD 且 body 非空,就必须抛出 TypeError

参见 WHATWG Fetch Standard 的 Request 构造算法

XMLHttpRequest:可以调用 send(body),但 GET / HEAD 的 body 会被丢弃

const xhr = new XMLHttpRequest();

xhr.open("GET", "/api/user");
xhr.send(JSON.stringify({ name: "asuhe" }));

调用本身可以写,但 XMLHttpRequest 标准规定:

如果 request method 是 GET 或 HEAD:
    body = null

也就是说,你传进去的 body 并不会真正成为 GET request body。

参见 WHATWG XMLHttpRequest Standard

jQuery $.ajax()GET + data 通常也不是 body

$.ajax({
  url: "/api/user",
  method: "GET",
  data: {
    name: "asuhe",
    age: 27
  }
});

jQuery 会把 data 转成 URL query string:

GET /api/user?name=asuhe&age=27

而不是:

GET /api/user

name=asuhe&age=27

参见 jQuery $.ajax() 官方文档

curl 则确实能制造 GET + body

例如:

curl -X GET -d 'hello=world' https://example.com

curl 官方 FAQ 直接把这种请求作为一个“少见但可以构造”的例子。

参见 curl FAQ

一句话总结

GET + body

HTTP framing        ✅ 可以
RFC 通用语义         ❌ 没有
RFC 推荐             ⚠️ SHOULD NOT 随便发
fetch()             ❌ TypeError
XMLHttpRequest      ❌ body 被置为 null
jQuery GET + data   ➡️ 通常转 query string
curl                ✅ 可以真正构造
代理/CDN/WAF         ⚠️ 可能出兼容性问题

2. HEAD 也不是“报文绝对不能有 body”,但 request body 同样没有通用语义

HEAD 与 GET 类似。

RFC 9110 同样说明:

  • framing 不因 HEAD 而失去表达 request content 的能力;
  • HEAD request content 没有通用语义;
  • 客户端 SHOULD NOT 随意发送。

浏览器方面:

  • fetch():HEAD + body → TypeError
  • XMLHttpRequest:HEAD 的 send(body) → body 被置为 null

参见 RFC 9110 §9.3.2

3. DELETE 可以带 request body,而且浏览器真的可以发

这和 GET 很容易被错误地混为一谈。

RFC 9110 对 DELETE 的 request content 同样说:

  • framing 本身不禁止;
  • DELETE content 没有通用定义的语义
  • 客户端 SHOULD NOT 随便发送;
  • 某些实现可能因为 request smuggling 风险而拒绝它。

Fetch 并没有像 GET / HEAD 那样禁止 DELETE + body

所以浏览器里可以:

fetch("/api/users", {
  method: "DELETE",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    ids: [1, 2, 3]
  })
});

这是真正的 request body。

如果是跨源请求,由于 DELETE 不是 CORS-safelisted method,它会受 CORS preflight 约束。浏览器命中 preflight cache 时可能不会再发一次 OPTIONS 网络请求。

参见 RFC 9110 §9.3.5

Fetch 中的 CORS-safelisted methods 只有 GETHEADPOST,参见 WHATWG Fetch §Methods

还有一个容易忽略的点:DELETE response 不可缓存

即使:

DELETE /foo

HTTP/1.1 204 No Content

204 这个状态码本身属于“heuristically cacheable”状态码,也不能推出这个 DELETE response 可以缓存

因为 RFC 对 DELETE method 有更具体的规则:

Responses to the DELETE method are not cacheable.

method 的缓存规则优先约束整个 response。

4. OPTIONS 可以带 body,但 HTTP 没规定这个 body 是干什么的

RFC 9110 对 OPTIONS 很特别:

  • OPTIONS request 可以有 content;
  • 如果有 content,客户端必须发送合法的 Content-Type
  • 但 HTTP 本身没有定义这些 content 的用途

例如:

OPTIONS /api HTTP/1.1
Content-Type: application/json

{"feature":"foo"}

报文可以成立,但到底是什么意思,需要应用协议自己定义。

参见 RFC 9110 §9.3.7

5. OPTIONS 不等于 CORS Preflight

这是前端开发中极其常见的概念混淆。

关系应该理解为:

OPTIONS

    └── HTTP 本身定义的一个 method

CORS preflight

    └── 浏览器使用 OPTIONS 实现的一种跨域安全检查流程

例如普通 OPTIONS:

OPTIONS /api HTTP/1.1

并不天然就是 CORS preflight。

典型的 CORS preflight 会包含:

OPTIONS /api HTTP/1.1
Origin: https://foo.example
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Content-Type

因此:

所有 CORS preflight 都使用 OPTIONS,但不是所有 OPTIONS 都是 CORS preflight。

参见 WHATWG Fetch Standard

6. TRACE 才是 request body 被协议明确禁止的典型例子

和 GET / DELETE 的“没有通用语义”不同,TRACE 是明确的:

A client MUST NOT send content in a TRACE request.

也就是说:

TRACE / HTTP/1.1

hello

不是简单的“不推荐”,而是违反 HTTP 语义要求。

而浏览器 Fetch 又更进一步,把:

  • CONNECT
  • TRACE
  • TRACK

定义为 forbidden methods,网页脚本不能通过 Fetch 构造这些 method。

参见 RFC 9110 §9.3.8WHATWG Fetch §Methods

7. GET 是 safe,不代表服务器绝对不能发生任何变化

很多人把 safe 理解为:

执行 GET 后服务器不能产生任何变化。

这也是错的。

RFC 对 safe method 的定义关注的是:

客户端没有请求、也不期待服务器状态发生改变。

因此一次 GET 完全可能导致:

  • 写访问日志;
  • 增加 PV / UV 统计;
  • 更新监控数据;
  • 更新缓存;
  • 广告点击计费;
  • 风控记录。

这些都属于实现产生的副作用,并不自动让 GET 失去 safe 属性。

真正错误的是设计成:

GET /user?id=123&action=delete

因为此时客户端实际上是在通过 GET 请求执行破坏性动作

参见 RFC 9110 §9.2.1

8. 幂等不等于“每次响应都一样”

RFC 对 idempotent 的定义是:

对服务器而言,多次相同请求的 intended effect 与只执行一次相同。

因此:

DELETE /users/1

第一次:

204 No Content

第二次:

404 Not Found

完全可能仍然是幂等的。

因为最终状态都是:

/users/1 已不存在

HTTP 幂等关注的是预期效果,不是:

  • status code 必须相同;
  • response body 必须相同;
  • 日志不能新增;
  • 时间戳不能变化。

参见 RFC 9110 §9.2.2

RFC 9110 定义的 method 中:

  • safe methods 是幂等的;
  • PUT 是幂等的;
  • DELETE 是幂等的。

9. POST 并不是“绝对不能缓存”

“GET 可缓存,POST 不可缓存”是非常常见的教学简化。

RFC 9110 实际允许 POST response 被缓存,但要求比较严格:

  1. response 包含显式 freshness 信息
  2. Content-Location 与 POST target URI 相同。

满足这些条件后,一个缓存的 POST response 可以用于以后满足相同 URI 的 GET 或 HEAD

但注意:

一个新的 POST request 不能直接由先前缓存的 POST response 来满足,因为 POST 本身可能是不安全的。

参见 RFC 9110 §9.3.3

而 RFC 9111 还指出:

实际中很多常见 HTTP cache 只缓存 GET response。

所以这里又是:

RFC:              POST response 在特定条件下可缓存
通用缓存实现:      未必支持
CDN 默认配置:       更未必

参见 RFC 9111 §2

10. 404 也可能被缓存

HTTP 不仅能缓存成功结果,也能缓存“负结果”。

RFC 9110 中明确列出的 heuristically cacheable 状态码包括:

200
203
204
206
300
301
308
404
405
410
414
501

因此一个普通 GET:

GET /foo

返回:

404 Not Found

即使没有显式 max-age,缓存也可能为它计算 heuristic freshness。

这意味着一种真实问题:

第一次:
GET /foo → 404

404 被缓存

随后服务端创建 /foo

再次 GET /foo → 仍可能命中旧 404

参见 RFC 9110 §15.1RFC 9111 §4.2.2

但“404 可启发式缓存”不等于任何 404 都能缓存

最终是否允许存储,还要综合:

  • request method;
  • Cache-Control: no-store
  • shared cache 下的 private
  • Authorization
  • 其他缓存规则。

所以:

status code 可缓存

只是判断条件之一,不是最终结论。

11. URL 有 ?query=... 并不会自动禁止 heuristic caching

旧版 HTTP 规范曾禁止缓存对带 query component 的 URI 使用 heuristic freshness,例如:

/api/user?id=123

但 RFC 9111 特别指出:

这一限制在实践中并没有得到广泛实现,因此现在不再采用这种通用禁止。

所以:

URL 里有 ?

并不能推导出:

这个 GET 一定不会被缓存

如果服务端明确不想被缓存,应该使用明确的 Cache-Control

参见 RFC 9111 §4.2.2

12. HEAD response 没有 body,却可以出现非零 Content-Length

例如:

HTTP/1.1 200 OK
Content-Length: 123456

这是合法的 HEAD response。

这里的:

Content-Length: 123456

不是说 HEAD 真发送了 123456 字节 body。

它表示:

如果相同请求使用 GET,本来会发送 123456 字节 content。

参见 RFC 9110 §8.6

对 conditional GET 返回的 304 Not Modified 也可以出现 Content-Length,但其值必须等于同一请求如果返回 200 OK 时 content 的字节数;不能把它理解为 304 实际携带了 content。

13. 204 No Content 不能有 body、trailer,甚至不能有 Content-Length

204 No Content 并不是:

body 长度必须为 0。

而是更严格:

  • response 不能包含 content;
  • 不能包含 trailers;
  • server MUST NOT 发送 Content-Length

因此:

HTTP/1.1 204 No Content
Content-Length: 0

严格按 RFC 9110 也不应该发送 Content-Length

但 204 可以携带有意义的 metadata,例如:

HTTP/1.1 204 No Content
ETag: "abc123"

参见 RFC 9110 §8.6RFC 9110 §15.3.5

204 本身可以 heuristically cacheable,但仍要看 method

例如:

GET /foo → 204

状态码层面允许 heuristic caching。

但:

DELETE /foo → 204

不能因为是 204 就缓存,因为 DELETE response 明确不可缓存。

因此正确判断顺序不是:

状态码 → 得出结论

而是:

method
+ status code
+ Cache-Control
+ Authorization
+ cache 类型
+ 其他规则
→ 最终判断

14. 301 / 302 可能把 POST 变成 GET

这是 HTTP 最著名的历史兼容包袱之一。

RFC 9110 允许用户代理在自动跟随:

POST

301 / 302

时将后续请求改成:

GET

所以:

POST /submit

302 Location: /result

GET /result

是合法而且极其常见的行为。

参见 RFC 9110 §15.4.2RFC 9110 §15.4.3

303 更明确地用于“转成 retrieval request”

303 的核心用途之一是转向 retrieval request:对 HEAD 请求仍使用 HEAD,其他场景中用户代理通常使用 GET。

POST → 303 See Other → GET
HEAD → 303 See Other → HEAD

参见 RFC 9110 §15.4.4

如果必须保留 method,用 307 / 308

POST → 307 → POST
POST → 308 → POST

因此:

Status是否可能 POST → GET常见用途
301永久重定向,带历史兼容行为
302临时重定向,带历史兼容行为
303是,明确转 retrieval操作完成后跳到结果资源
307临时重定向且保留 method
308永久重定向且保留 method

对于:

  • 文件上传;
  • 支付;
  • RPC;
  • 有 request body 的操作接口;

307 / 308 和 301 / 302 不能机械互换。

参见 RFC 9110 §15.4

15. HTTP Method 大小写敏感,但 Header 名大小写不敏感

Method:大小写敏感

HTTP 规范明确规定 method token 是 case-sensitive。

所以协议语义上:

GET ≠ get
PATCH ≠ patch

标准 method 只是约定使用大写

参见 RFC 9110 §9

Header field name:大小写不敏感

下面三种在 HTTP 语义上是同一个 field name:

Content-Type
content-type
CONTENT-TYPE

参见 RFC 9110 §5.1

16. 但 Fetch 会偷偷帮部分 method 转成大写

WHATWG Fetch 为了历史兼容,会自动 normalize:

DELETE
GET
HEAD
OPTIONS
POST
PUT

例如:

fetch("/", {
  method: "get"
});

会被规范化为:

GET

但并不是所有 method 都这样处理。

一个特别有意思的例子:

fetch("/", {
  method: "patch"
});

Fetch 并不会把它自动变成 PATCH

因此服务器可能真的收到:

patch / HTTP/1.1

而不是:

PATCH / HTTP/1.1

Fetch 标准甚至专门提醒:

patch 很可能得到 405,而 PATCH 更可能成功。

参见 WHATWG Fetch §Methods

这就是:

HTTP:Method 大小写敏感

Fetch:只对部分历史常见 method 做兼容性 normalization

17. HTTP/2 语义上 Header 名仍大小写不敏感,但线上必须使用小写

在 HTTP 通用语义层:

Content-Type
content-type

表示同一个字段。

但 HTTP/2 在编码消息时要求:

Field names MUST be converted to lowercase.

所以:

content-type
cache-control
user-agent

才是 HTTP/2 在线上的形式。

含大写字母的 HTTP/2 field name 会使消息成为 malformed message。

参见 RFC 9113 §8.2

这也是为什么 Chrome DevTools 里经常看到全部小写的 header。

18. HTTP/2 里 Connection: keep-alive 不是“多余”,而是非法

HTTP/1.1 时代经常看到:

Connection: keep-alive

但 HTTP/2 明确禁止 connection-specific header fields:

Connection
Proxy-Connection
Keep-Alive
Transfer-Encoding
Upgrade

包含这些字段的 HTTP/2 message 必须被视为 malformed。

参见 RFC 9113 §8.2.2

TE 有唯一例外

HTTP/2 request 可以出现:

TE: trailers

但不能是:

TE: gzip

所以很多 HTTP/1.1 时代背下来的 header 规则不能机械搬到 H2。

19. 405 和 501 都和 method 有关,但含义完全不同

405 Method Not Allowed

表示:

服务器认识并实现这个 method
但 target resource 不允许使用

例如:

DELETE /users

服务器整体支持 DELETE,但是 /users 不允许:

405 Method Not Allowed
Allow: GET, POST

RFC 规定 405 response MUST 包含 Allow

501 Not Implemented

表示:

服务器不认识 / 没有实现完成这个请求所需的功能

尤其适合:

FOOBAR /users

服务器根本不支持 FOOBAR method。

可以这样记:

405:
“这个动作我会,但这个资源不让你这么干。”

501:
“这个动作我根本不会。”

参见 RFC 9110 §15.5.6RFC 9110 §15.6.2

20. 2026 年 HTTP 正式有了 QUERY Method

过去复杂查询经常陷入一个尴尬:

GET

GET /search?filters=非常复杂的一大坨数据

优点:

  • safe;
  • idempotent;
  • 很适合缓存。

缺点:

  • 查询数据必须塞进 URI;
  • 长度和日志暴露等问题;
  • GET body 又没有通用语义。

POST

POST /search
Content-Type: application/json

{
  "filters": {
    "status": "active"
  }
}

body 很方便,但从通用 HTTP 语义看 POST:

  • 不一定 safe;
  • 不一定 idempotent;
  • 中间设施不能只看到 method 就知道这是纯查询。

QUERY

RFC 10008 在 2026 年 6 月发布,正式定义:

QUERY /search HTTP/1.1
Content-Type: application/json

{
  "age": {
    "gte": 18
  },
  "cities": [
    "Shanghai",
    "Tokyo"
  ]
}

QUERY 明确定义为:

Safe        ✅
Idempotent  ✅
Response 可缓存 ✅(仍受 RFC 9111 与实现支持约束)
Request body / content 有明确意义 ✅

每个 QUERY 请求都必须携带与 request content 一致的 Content-Type。如果字段缺失,或声明的媒体类型与 content 不一致,服务器必须使请求失败。

参见 RFC 10008 §1RFC 10008 §2

QUERY 的缓存 key 必须包含 request content

这很合理:

QUERY /users
body = {"age":18}

和:

QUERY /users
body = {"age":30}

显然不能命中同一个缓存结果。

因此 RFC 10008 明确要求 QUERY cache key 纳入:

  • request content;
  • 与 content 相关的 metadata。

参见 RFC 10008 §2.7

浏览器层面

Fetch 的 method token 和 forbidden-method 规则只明确禁止:

CONNECT
TRACE
TRACK

因此从这一层规则看,new Request(url, { method: "QUERY", body }) 可以被构造;真正发送仍受浏览器实现、CORS 与整条网络栈支持情况影响。

但 QUERY 不属于 CORS-safelisted methods:

GET
HEAD
POST

所以跨源 QUERY 会受 CORS preflight 约束;命中 preflight cache 时不一定重新发送 OPTIONS

RFC 10008 甚至专门说明了这一点。

参见 RFC 10008 §4

真正的问题是生态兼容性

RFC 10008 是 2026 年的新规范,因此即使标准已经存在:

Browser

CDN

WAF

API Gateway

Reverse Proxy

Web Framework

Application

其中任何一层都可能还不认识或没有正确支持 QUERY

所以现阶段不能简单理解成:

“有 RFC 了,生产环境立刻就能放心用。”

总结:判断 HTTP“能不能这么干”的正确方法

以后遇到类似问题,不要只问:

“HTTP 支不支持?”

而是按下面顺序判断:

① 报文格式能否表达?

② RFC 给这种组合定义了什么语义?

③ RFC 是 MUST / SHOULD / MAY 中哪一级?

④ 浏览器 Fetch / XHR 是否进一步限制?

⑤ 是否触发 CORS?

⑥ Proxy / CDN / WAF 是否支持?

⑦ Web Server / Framework 是否支持?

⑧ 最终应用是否自己定义了额外语义?

例如 GET + body

HTTP framing       ✅
RFC 通用语义        ❌
RFC 建议            ⚠️ SHOULD NOT
Fetch              ❌
XHR                ❌ 实际 body 被丢弃
jQuery Ajax        ➡️ data 通常转 query string
curl               ✅
Proxy / CDN / WAF  ⚠️ 可能拒绝或误处理
Server Framework   ⚠️ 看具体实现

这套分层思维比背:

GET 不能有 body
POST 不能缓存
DELETE 一定没 body
OPTIONS 就是 CORS
GET 完全不能改服务器
幂等就是每次响应相同

要准确得多。

官方参考资料

HTTP 核心规范

  1. RFC 9110 — HTTP Semantics

    重点章节:

  2. RFC 9111 — HTTP Caching

    重点章节:

  3. RFC 9113 — HTTP/2

    重点章节:

浏览器 Web 平台规范

  1. WHATWG Fetch Standard,其中 Methods 一节包含:

    • CORS-safelisted methods:GET / HEAD / POST
    • forbidden methods:CONNECT / TRACE / TRACK
    • method normalization;
    • GET / HEAD + body 的 TypeError 规则。
  2. WHATWG XMLHttpRequest Standardsend() 对 GET / HEAD 会把 body 置为 null

QUERY Method

  1. RFC 10008 — The HTTP QUERY Method

    重点章节:

工具与库行为

  1. jQuery $.ajax() 官方文档:GET 等请求中的 data 会追加到 URL,而不是作为 request body。

  2. curl FAQ:官方示例中展示可以使用:

    curl -X GET -d data URL

    构造带 request body 的 GET 请求。

最后一个最重要的记忆点

“协议格式允许” ≠ “RFC 赋予标准语义” ≠ “浏览器允许” ≠ “中间件支持” ≠ “生产环境值得这么做”。

这句话基本可以解释 HTTP 世界里绝大多数看起来“规范自相矛盾”的现象。