这个故障是这样的。某个集成向你的接口发送 POST。接口是好的,路由也在,手工测一遍全都正常。可日志里出现的却是对一条只接受 POST 的路由发起的 GET,返回 404 或 405。
原因几乎总是同一个:客户端和接口之间横着一条 301 或 302 跳转。而这两个状态码并不要求客户端保留请求方法。
十秒就能验证:
curl -sS -o /dev/null -L -d 'test=1' \
-w '跳转后的方法:%{method}\n' \
'https://httpbingo.org/redirect-to?url=/anything&status_code=301'
输出:跳转后的方法:GET。请求体也一并丢失。
把同一条命令里的 301 换成 308,得到的就是 POST。差别只在状态码。
五个状态码,两个属性
跳转在两个彼此独立的维度上有区别:是永久还是临时,以及请求方法是否保留。
| 状态码 | 名称 | 持久性 | 保留方法 |
|---|---|---|---|
| 301 | Moved Permanently | 永久 | 否 |
| 302 | Found | 临时 | 否 |
| 303 | See Other | — | 否,始终为 GET |
| 307 | Temporary Redirect | 临时 | 是 |
| 308 | Permanent Redirect | 永久 | 是 |
注意其中的对称性:307 是带方法保证的 302,308 是带方法保证的 301。
这种混乱从何而来
从规范上讲,301 和 302 从来没有要求过更换方法。但早期的浏览器和客户端恰恰是这么做的,等到这个偏差被注意到时,整个网络早已按这种方式运转。
于是这种行为被写进规范,作为「允许」:现行的 HTTP 语义规范 RFC 9110 允许客户端在 301 和 302 时把 POST 换成 GET。是允许,而非要求——也就是说行为交由客户端自行决定,因此不能依赖。
为了给出一个明确无歧义的选项,才引入了 307 和 308。对它们而言更换方法是被禁止的:客户端必须以相同的方法和相同的请求体重发请求。308 由独立的 RFC 7538 定义,MDN 文档中也有参考说明。
实际结论:只有在请求确定是 GET 的地方,301 和 302 才是安全的。 凡是可能收到 POST 的地方,都需要 307 或 308。
第二个陷阱:永久跳转会被缓存
这一点比方法问题更容易被忘记。
301 和 308 声明的是永久迁移,浏览器有权将其缓存很长时间——有时要到用户清空缓存为止。此后服务器根本不会再被询问。
由此得出一条规则:错误的 301 几乎无法回滚。 你会去改配置,但已经拿到旧响应的访客仍会继续被跳转。302 和 307 没有这个问题,它们按定义就是临时的。
实用建议:在迁移方案尚未定型之前,使用临时状态码。日后把 302 提升为 301 很容易,反过来则不然。
该怎么选
页面永久迁移,且只接受 GET → 301。典型场景:旧文章网址、网址结构调整。
同上,但接口接受 POST → 308。API、Webhook、表单都属于这一类。
目标地址临时变更,只有 GET → 302。
临时变更,且方法必须保留 → 307。
POST 处理完之后把用户送到结果页 → 303。这就是 POST/Redirect/GET 模式:它有意把请求变成 GET,好让刷新页面不会重复提交表单。这里更换方法是目的,不是缺陷。
实践中它在哪里出问题
API 前面的 http→https 跳转。 最常见也最难受的情形。客户端配置的是 http://,服务器返回 301 指向 https://,方法随之丢失。症状是:日志里出现对 POST 路由的 GET。把 301 换成 308 即可解决。
主机名规范化。 www 到非 www(或反之)的跳转通常配成 301。对页面没问题,但如果同一主机上还住着 API,问题完全一样。
表单提交。 表单被提交到一个会跳转的地址。用户看到的是「什么都没发生」,因为数据根本没到达处理程序。
Webhook。 外部服务向你当初提供的地址发送 POST。如果那个地址经由 301 迁移了,Webhook 会悄无声息地停止工作,而发送方的日志里显示的是对跳转的成功响应。
这四种情形有共同特征:接口本身完好,但请求没有以原本的形态到达。于是排查方向就跑偏了——大家去处理程序里找 bug,而问题在 Web 服务器配置里。
短链服务为什么用 302
短链接几乎总是返回 302,这是有意的选择,而不是疏忽。
原因在于持久性,而不在方法:短链接的目标地址可能变更。用 301 就等于允许浏览器把这次跳转永久缓存,而在更换目标之后,一部分访客仍会落到旧地址上。这里方法无关紧要——短链接是用 GET 请求访问的。
机制本身另有说明,见短网服务是怎么工作的。
状态码的选择不会改变什么
两个流传很广的误解。
链接权重。 在 301 和 302 之间选择并不影响权重传递——Google 早在 2016 年就已确认。完整讨论见短链接会传递链接权重吗。
规范网址的选定。 跳转和 rel="canonical" 解决的是不同问题:跳转让一个地址退出流通,canonical 让两个地址都可访问并指明主地址。何时用哪个,见 canonical 完整指南。
简短清单
- 在接受
POST的路由上使用 307 或 308,而不是 302 和 301。 - 单独检查 http→https 跳转:它常由一条通用规则设定,而 API 会被遗漏。
- 方案尚未定型时使用临时状态码。301 和 308 会被缓存,几乎不可逆。
- 表单处理完之后用 303——这是正确行为,不是错误。
- 把跳转链条压缩到一跳:每多一跳都同时增加延迟和风险。