bigywater.tech
网络协议 · 82

重定向:301 / 302 / 307 / 308 的区别

重定向看起来只是“换个地址”,但 301、302、303、307、308 对请求方法和请求体的处理并不一样,不同的 HTTP 客户端库处理 301 / 302 的方式甚至互相矛盾。这篇用本机服务器逐个试,再看循环重定向的上限和 Location 的各种写法。

POST+5B vs GET+0B
同一个 302,urllib3 与 requests 在 /final 收到的方法和请求体
31 / 4 / 11
requests / urllib3 / urllib.request 在循环重定向里放弃前服务器收到的 /loop 请求数
1 次
http://python.org/ 在 requests 里要经历的重定向(301 到 https://www.python.org/)

真实重定向

实测 · 本机执行输出
  http://www.python.org/ → 301 Moved Permanently,Location = https://www.python.org/,消息体 0 字节,用时 1014 ms;Cache-Control = None;Content-Type = None
  http://python.org/ → 301 Moved Permanently,Location = https://www.python.org/,消息体 0 字节,用时 1030 ms;Cache-Control = None;Content-Type = None
  requests 跟随 http://python.org/:经过 1 次重定向,最终 200 https://www.python.org/,总用时 1187 ms;跳转链 [(301, 'https://www.python.org/')]

python.org 明文请求 301 到 https://www.python.org/,同时升级 HTTPS 并统一 www。

方法与请求体

实测 · 本机执行输出
    状态码   requests                urllib3                 urllib.request     (每格:最终到达 /final 时的 方法+请求体字节数)
    301      GET+0B                 POST+5B                GET+0B
    302      GET+0B                 POST+5B                GET+0B
    303      GET+0B                 GET+0B                 GET+0B
    307      POST+5B                POST+5B                异常 HTTP 307
    308      POST+5B                POST+5B                异常 HTTP 308
  规范(RFC 9110)的意图:301 / 302 历史上浏览器把 POST 改成 GET;303 明确要求改成 GET;307 / 308 必须保持方法和请求体。

301 / 302 各库不一致:requests、urllib.request 改成 GET,urllib3 保持 POST;303 都变 GET;307 / 308 保持方法(urllib.request 不跟随)。

循环

实测 · 本机执行输出
  requests        :服务器共收到 31 次 /loop 请求后客户端放弃,异常 TooManyRedirects
  urllib3 默认      :服务器共收到 4 次 /loop 请求后客户端放弃,异常 MaxRetryError
  urllib.request  :服务器共收到 11 次 /loop 请求后客户端放弃,异常 HTTPError

各库默认的最大重定向数不同:urllib3 3,urllib.request 10,requests 30。

Location

实测 · 本机执行输出
  Location: /abs/path                         → 最终请求 /abs/path;最终 URL http://127.0.0.1:<端口>/abs/path
  Location: rel/path                          → 最终请求 /rel/path;最终 URL http://127.0.0.1:<端口>/rel/path
  Location: ../up                             → 最终请求 /up;最终 URL http://127.0.0.1:<端口>/up
  Location: //127.0.0.1:9400/proto-rel        → 最终请求 /proto-rel;最终 URL http://127.0.0.1:<端口>/proto-rel
  Location: /with?x=1#frag                    → 最终请求 /with?x=1;最终 URL http://127.0.0.1:<端口>/with?x=1#frag

可以是绝对路径、相对路径、父目录或协议相对 URL;片段不发给服务器。

分步演示

重定向
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。第一节是对 python.org 的明文请求与 requests(2.34.2);第二至四节用本机脚本服务器与 requests / urllib3 2.8.0 / urllib.request;浏览器的行为我没有直接测
    • 没有验证:301 / 308 的缓存(浏览器会永久缓存);HSTS 与 301 的配合;跨域重定向时 Authorization 头是否被保留;Refresh 头与 这类非 HTTP 重定向;重定向后 Referer 的处理;POST 301 在浏览器里的真实行为(只测了库)
    京ICP备2026067502号-1京公网安备11010502063487号