bigywater.tech
网络协议 · 89

状态码与错误处理:4xx / 5xx 在客户端的真实表现

状态码是 HTTP 里最浓缩的一个数字。客户端到底该怎么对待它们?404 要不要抛异常?429 / 503 之后什么时候重试?一个大请求体应不应该等服务器先说“继续”?这篇用真实的 httpbin 响应和本机服务器逐个验证。

2.01 秒
服务器回 429 + Retry-After: 2 时,urllib3 两次请求之间等待的时间
0 / 1 / 2 / 4 秒
503 且没有 Retry-After、backoff_factor = 0.5 时 urllib3 相邻请求的间隔
5000000 → 0 字节
未认证上传 5 MB 时,不带与带 Expect: 100-continue 客户端实际发出的请求体

状态码的家族

实测 · 本机执行输出
    码    原因短语                      消息体字节   特殊头部
    200   OK                                0    {}
    201   CREATED                           0    {}
    204   NO CONTENT                        0    {}
    301   MOVED PERMANENTLY                 0    {'location': '/redirect/1'}
    302   FOUND                             0    {'location': '/redirect/1'}
    304   NOT MODIFIED                      0    {}
    400   BAD REQUEST                       0    {}
    401   UNAUTHORIZED                      0    {'WWW-Authenticate': 'Basic realm="Fake Realm"'}
    403   FORBIDDEN                         0    {}
    404   NOT FOUND                         0    {}
    405   METHOD NOT ALLOWED                0    {}
    409   CONFLICT                          0    {}
    410   GONE                              0    {}
    418   I'M A TEAPOT                    135    {}
    429   TOO MANY REQUESTS                 0    {}
    500   INTERNAL SERVER ERROR             0    {}
    501   NOT IMPLEMENTED                   0    {}
    502   BAD GATEWAY                       0    {}
    503   SERVICE UNAVAILABLE               0    {}
    504   GATEWAY TIMEOUT                   0    {}

2xx 成功、3xx 重定向、4xx 客户端的错、5xx 服务器的错;原因短语只给人看。

客户端对错误的定义

实测 · 本机执行输出
  404:
    requests.get:不抛异常,status_code = 404;r.ok = False;调用 raise_for_status() → 抛 HTTPError("404 Client Error: NOT FOUND for url: https://httpb")
    urllib.request.urlopen:抛 HTTPError 404
    urllib3.request(retries=False):不抛异常,status = 404
    http.client:不抛异常,status = 404
  500:
    requests.get:不抛异常,status_code = 500;r.ok = False;调用 raise_for_status() → 抛 HTTPError("500 Server Error: INTERNAL SERVER ERROR for url: h")
    urllib.request.urlopen:抛 HTTPError 500
    urllib3.request(retries=False):不抛异常,status = 500
    http.client:不抛异常,status = 500

requests、urllib3、http.client 对 404 / 500 都不抛异常;urllib.request 会抛;不检查状态码是常见 bug。

Retry-After 与退避

实测 · 本机执行输出
  429 + Retry-After: 2:客户端第 2 次请求拿到 200;两次请求到达服务器的间隔 2.01 秒(服务器要求等 2 秒)
  503 前 4 次失败(没有 Retry-After),backoff_factor = 0.5:客户端第 5 次请求拿到 200;相邻请求的间隔(秒)[0.0, 1.0, 2.01, 4.01](指数退避:0 / 1.0 / 2.0 / 4.0 附近;urllib3 第一次重试不等)
  不开重试时,同样的 503/200 序列只发 1 次请求:状态 200

有 Retry-After 就听服务器的;没有就指数退避。

Expect: 100-continue

实测 · 本机执行输出
  不带 Expect:客户端把 5 MB 都发了(客户端写出 5000000 字节),服务器读了 5000000 字节后拒绝;客户端收到的第一行 'HTTP/1.1 401 Unauthorized'
  带 Expect: 100-continue(未认证):服务器直接回 'HTTP/1.1 401 Unauthorized',客户端没有发请求体(写出 0 字节),服务器读到的请求体 0 字节——省下 5 MB

大请求体前先问服务器,被拒绝时省下整个请求体。

分步演示

状态码与错误处理
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。第一、二节是对 httpbin.org 的 HTTPS 请求(原因短语和 429 没有 Retry-After 是它的行为);第三、四节用本机脚本服务器和 urllib3 2.8.0 / requests;没有测浏览器对状态码的处理
    • 没有验证:浏览器遇到各状态码的表现(例如 401 弹认证框、5xx 的错误页);100 Continue 在 HTTP/2 里的行为;自动重试对非幂等请求的风险(w77 讲过);408、425、451 等较少见的状态码;带 Retry-After 为日期格式的情形;服务端限流(429)的实现方式
    京ICP备2026067502号-1京公网安备11010502063487号