bigywater.tech
网络协议 · 77

HTTP 方法的语义:安全性、幂等性与重试

GET、POST、PUT、DELETE……HTTP 方法不只是动词,它们携带着“安全吗?幂等吗?能重试吗?”的约定。这篇先看一个真实服务器对每个方法的态度,再用一个“响应丢了”的模拟场景证明:为什么 GET 和 PUT 可以放心重试,而 POST 重试一次就可能把事情做两遍。

3 次 vs 1 次
响应丢失时,强制重试的 POST 与默认不重试的 POST 在服务器账上被执行的次数
405
example.com 对 POST / PUT / DELETE / OPTIONS / PATCH 的统一回答,且不带 Allow 头
0 字节
HEAD 响应的消息体(GET 为 589 字节)

方法只是标签

实测 · 本机执行输出
  GET     → HTTP/1.1 200 OK                         Allow: GET, HEAD  body 589 字节
  HEAD    → HTTP/1.1 200 OK                         Allow: GET, HEAD  body 0 字节
  POST    → HTTP/1.1 405 Method Not Allowed         Allow: None  body 570 字节
  PUT     → HTTP/1.1 405 Method Not Allowed         Allow: None  body 570 字节
  DELETE  → HTTP/1.1 405 Method Not Allowed         Allow: None  body 570 字节
  OPTIONS → HTTP/1.1 405 Method Not Allowed         Allow: None  body 570 字节
  PATCH   → HTTP/1.1 405 Method Not Allowed         Allow: None  body 570 字节
  TRACE   → HTTP/1.1 405 Not Allowed                Allow: None  body 155 字节

example.com 只接受 GET 与 HEAD,其它方法一律 405。

HEAD

实测 · 本机执行输出
  GET:HTTP/1.1 200 OK,头部 12 个,消息体 589 字节;HEAD:HTTP/1.1 200 OK,头部 11 个,消息体 0 字节
  值相同的头部:['accept-ranges', 'allow', 'alt-svc', 'cf-cache-status', 'connection', 'content-type', 'server'];不同或只在一边的头部(除 Date / Age / CF-RAY / Last-Modified):['transfer-encoding']
  GET 有 Transfer-Encoding = chunked,HEAD 的 Content-Length = None

HEAD 给出与 GET 相同的头部、没有消息体,不需要 Transfer-Encoding。

幂等与重试

实测 · 本机执行输出
  urllib3 的默认重试方法集合(只有这些方法会在连接出错后自动重试):['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT', 'TRACE']
  GET  :前 2 次连接在服务器处理后被复位 → 客户端 最终成功,状态 200;服务器共收到 3 次该请求,连接数 3
  PUT  :前 2 次连接在服务器处理后被复位 → 客户端 最终成功,状态 200;服务器共收到 3 次该请求,连接数 3
  POST :前 2 次连接在服务器处理后被复位 → 客户端 最终失败:ProtocolError;服务器共收到 1 次该请求,连接数 1
  POST,强制允许 POST 重试(allowed_methods=None):客户端 最终成功,状态 200;服务器的账上 POST 被执行了 3 次——前 2 次的响应丢了,但业务已经执行了,重试就重复执行了(这里是 "扣款" 的重复)

GET 和 PUT 重试 3 次结果同 1 次;POST 重试会重复执行,默认客户端库不重试它。

边缘情况

实测 · 本机执行输出
  小写 get                    → HTTP/1.1 400 Bad Request  Allow: None
  OPTIONS *                 → HTTP/1.1 400 Bad Request  Allow: None
  GET 带请求体(Content-Length: 5)→ HTTP/1.1 200 OK  Allow: GET, HEAD
  DELETE /                  → HTTP/1.1 405 Method Not Allowed  Allow: None

方法名区分大小写;OPTIONS * 被拒;GET 带请求体被忽略。

分步演示

HTTP 方法
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。第一、二、四节是对 example.com:80 的明文请求(PUT / POST / DELETE 带的是极小的无意义请求体);第三节用本机脚本服务器 + urllib3 2.8.0,“响应丢了”是用 SO_LINGER=0 的复位模拟的
    • 没有验证:PATCH、DELETE 在设计良好的 REST 服务上的真实语义;Idempotency-Key 之类的应用层幂等机制;浏览器对 POST 重新提交的提示;安全方法(GET)被错误地用来改状态时的后果(预取、爬虫);请求体里的 Expect: 100-continue
    京ICP备2026067502号-1京公网安备11010502063487号