2169 字
约 7 分钟
17
S02-A22 · 3xx 的语义:301、302、307 为什么会让"POST 变 GET"

一次"提交表单后跳转"的改造,把 302 换成了 301——理由是"永久跳转更规范"。上线后用户"提交后刷新"会重复提交(因为没有跳转),而浏览器缓存了 301,把"提交地址"永久指向了"结果页"。

3xx 家族有四个常用状态码,而它们的差别集中在两件事:是否允许"改变方法",这两件事的"组合"决定了风险:"永久 + 会改方法"(301)是最危险的组合——它既"记住了",又"改了语义"。
以及是否可缓存。


1. 四个状态码的关键差别

状态码 方法是否可变 可缓存
301 部分客户端变 GET 是(永久)
302 会变 GET 否(历史问题)
303 总是变 GET 否
307 不变(保持方法与 body) 否
308 不变 是(永久)

第二项是"经典坑":302 在历史实现里"大多数客户端会把 POST 变 GET",而标准文本并不强制——于是"实际行为"与"标准"不同。

第四项是"要保住 POST 时"的唯一选择:307 保证"方法不变、body 重发"。第四项的"前提"是"目标地址能接受同一个 body":**如果"跳转是为了"去另一个服务处理",那 307 会把原始 body 发过去——而那个服务可能不认识它。

2. 三种"该用哪个"的场景

场景 选择
提交表单后展示结果页 303(明确的"看那个")
域名/路径永久迁移 301 或 308
临时维护跳转 302 或 307
需要保留 POST body 307

第一项的"额外好处"是"不缓存":**于是"提交 → 看结果"这条路径,不会因为"某个 3xx 被缓存"而错乱——这正是"提交后跳转"最常见的需求。
第一项的"303"是"语义最清楚"的:它的定义就是"去另一个地址 GET 那个结果"——于是"提交后跳转"用 303 最不容易出错。

3. "301 被永久缓存"的三种后果

后果 说明
改回来很难 浏览器缓存了"永久"
提交地址被改写 表单指向了结果页
排查困惑 "我明明改了配置"

第二项就是开头的故障:**浏览器把"POST 到 /submit"永久记成"去 /result"**第二项的"传导"很彻底:连"表单页本身"都可能被绕过——用户点"提交"时浏览器直接去 /result,于是"没有提交",只有"看结果"。
——于是"用户怎么提交都没反应"。

// 三种跳转的实际行为(同一表单 POST)
301 → POST /submit:多数浏览器"变 GET 到 /result",且"永久记住"
302 → 同左(但理论上不保证,历史实现如此)
303 → "变 GET 到 /result"(语义正确,不永久)
307 → "保持 POST /result"(要保证 body 重发)
// 判据:这次跳转"是不是永久的",以及"方法要不要保留"

"提交后的跳转"推荐 303,而它和"前端路由跳转"是两件事:

POST /orders HTTP/1.1
Content-Type: application/json
{"sku":"A-12","qty":1}

→ HTTP/1.1 303 See Other
Location: /orders/O-1001        # 浏览器去 GET 这个地址
// 关键:body 已经处理完,跳转只是"去看结果",方法必须是 GET

4. "临时"与"永久"的分界

情形 该用
域名换新(旧域名永久保留) 301/308
页面临时维护 302/307(加 Cache-Control: no-store)
A/B 分流跳转 302(且不缓存)
提交后跳结果 303

第二项的"实践"是"默认给所有"临时跳转"加 no-store":**成本一个 header,收益是"改配置立刻生效"——而"没加"时要等 CDN 缓存过期。
第二项的"注意"是:即使 302 本身不该被缓存,某些代理/CDN 仍会缓存——所以要显式加 Cache-Control: no-store。

"跳转链"本身可以设上限,避免"无限循环":

// 两种"链"的处理
浏览器:多数限制 20 次跳转(超过即报 ERR_TOO_MANY_REDIRECTS)
客户端(API):应设"最多 3 次"并禁用"跨源跳转"
// 判据:每次跳转都"增加一次 RTT",超过 2 次就该合并成直接跳

5. "跳转链"的三个坑

坑 说明
多次跳转(A→B→C) 延迟叠加、易断
跳转丢失 header 跨域时 Authorization 会丢
跳转丢失 body 301/302 变 GET 后 body 没了

第二项在"API 跳转"里最痛:客户端跟着 3xx 走,但"认证头"不带过去第二项的"判别"是"看目标地址是否同源":同源时浏览器会带 cookie,跨源时不会——于是"跳转后 401"多半是"跨源"。
——于是"跳到新地址后 401"。

第三项的"表现"很具体:服务端日志里"跳转后的请求"没有 body,于是"参数全空"——而它看起来像"客户端没传参"。
第三项与"方法变化"是同一件事:变 GET 之后 body 被丢弃,于是"跳转后的请求看起来是空的"。

"301 被缓存"可以用"换路径"来解,而"换路径"要配合"旧的 301 改回 302":

// 修法:新提交路径 + 旧路径改回"临时"(不再被永久记住)
location = /submit {
    return 302 /submit-v2;        # 不再是 301
}
location = /submit-v2 {
    proxy_pass http://backend;    # 新路径承接提交
}
// 说明:已经缓存了 301 的浏览器仍会被带到 /result,但它们"至少不会再提交"

6. 排错:五种"跳转相关问题"

现象 成因 处理方向
提交后没反应 301 把 POST 变 GET 并缓存 改 303/307 + 清缓存
跳转后 401 认证头未跟随 同域跳转或用 307
改了配置不生效 永久缓存 用新路径/清 CDN
跳转链太长 多层重写 收敛到一层
刷新重复提交 无"提交后跳转" 用 303 跳结果页

第一项是"最难修"的:浏览器缓存了 301,只有"清缓存"或"换 URL"能救第一项的"修法"很现实:换一个新的提交路径(/submit-v2),于是"旧缓存"不再被命中——而"清缓存"这种"让用户操作"的方案通常不可行。
——而"用户不会清缓存"。

最后一个"测试"建议:用 curl -I -L 看"跳转链"与"最终方法",比"在浏览器里点"更容易发现方法变化。

// 三步验证跳转语义(command-line 更清楚)
curl -sI http://host/submit | head -3                 # 状态码
curl -sI -L -X POST http://host/submit | head -5      # 跟随后的状态
curl -s -L -X POST -d '{"a":1}' http://host/submit -v | grep -E '^> (POST|GET)'
// 关键:最后一行能看到"跟随之后用的是 POST 还是 GET"

7. 边界:什么情况不必纠结状态码

场景 为什么 做法
内部 API(客户端自己实现跳转) 不依赖 3xx 用 200 + 响应体
SPA 前端路由 跳转在前端 只配 index.html 回退
单页无跨域 302 够用 302
静态资源迁移(已有 CDN 规则) CDN 处理 跟随规则

最后一条与"可观测"有关的边界:跳转应当在"访问日志"里留痕(源路径、目标、状态码)。否则"用户说没反应"时,你只能看到"没有请求到达源路径"——而"为什么没到达"无从判断。
判断口径一句话:"这次跳转会不会被"浏览器或代理"记住,以及"要不要保住方法与 body"。会被记住(永久迁移),用 301/308 且"想清楚后果";要保方法(提交类),用 307;只是"看另一个页面",用 303。


8. 自测

  1. 五个状态码的关键差别是哪两件事?为什么 302 是"经典坑"?
  2. "提交后跳转"为什么该用 303?它的语义是什么?
  3. 301 被永久缓存后,三种后果分别是什么?为什么"用户不会清缓存"让问题难修?
  4. "临时维护"为什么除了 302 还要加 Cache-Control: no-store?
  5. "跳转丢失 header/body"的两个机制分别是什么?
  6. 判断"该用哪个 3xx"的两个问题是什么?
S02-A22 · 3xx 的语义:301、302、307 为什么会让"POST 变 GET"
https://www.clxhxhhr.top/posts/6618/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。