HTTP 有两个地方几乎每个后端都被问过,而答案往往停在"背下来就行":
- 重定向:301 和 302 到底谁会自动跳?
- 5xx:502 和 504 怎么一眼分开?
- 方法:为什么更新资源要用 PUT,而不是 POST?
这三个问题的共同点是,它们都不考记忆,考的是协议里那条被忽略的语义。把语义抓住,答案自己就出来了。
本文按这三条线展开,每条给出判据、真实现场的坑,以及可直接抄用的排查/选型表。
零、结论速览
| 问题 | 一句话答案 |
|---|---|
| 301/302 谁自动跳? | 都会跳。 跳不跳是客户端的默认行为,与"永久/临时"无关 |
| 301 与 302 差在哪? | 缓存与语义。 301 被浏览器持久记住,302 默认不记 |
| 502 与 504 差在哪? | 上游有没有回应。 502 回了但无效;504 压根没回 |
| 502/504 谁发出的? | 网关(nginx 等)发出的,不是上游自己发的 |
| 为什么用 PUT 更新? | 幂等。 重试安全,这是分布式系统里的硬需求 |
| PUT 是"更新"吗? | 不是。 PUT 是整体替换,局部更新是 PATCH |
一、301 与 302:都会自动跳转,区别不在跳转本身
1.1 先破一个流传很广的误解
一个很常见的说法是:301 是永久重定向,所以浏览器会记住并自动跳;302 是临时重定向,所以不会自动跳,要手动处理。
这个说法前半句对、后半句错。
301 和 302 都会自动跳转。 在浏览器里输入原地址,两者都会把页面带到新地址,用户看不出差别。
“永久/临时"这组词描述的是这个跳转要不要被缓存,不是"要不要跳”。
1.2 自动跟随是客户端行为,协议并不强制
RFC 9110 对 3xx 的规定只有一条核心要求:响应里给一个 Location 指向新地址。至于要不要跟随,由客户端自己决定。
所以"会不会自动跳"取决于用什么工具发请求:
| 客户端 | 是否自动跟随 | 说明 |
|---|---|---|
| 浏览器(Chrome / Edge / Firefox) | 会 | 连续跳转上限约 20 次,超出报 ERR_TOO_MANY_REDIRECTS |
curl | 不会 | 必须显式加 -L / --location |
Python requests | 会 | 默认 allow_redirects=True |
| Postman | 会 | 可在设置中关闭自动重定向 |
Java HttpClient | 会 | 但默认策略不跟随 HTTPS 到 HTTP 的降级 |
一个很典型的误判场景:用 curl 请求接口,看到响应体是 301/302 的 HTML,就认为"这个 302 没有跳转"。实际上是 curl 本身不去跟随,加 -L 立刻就能看到最终结果。
判断跳转行为,永远先看客户端策略,再看状态码。
1.3 真正的分岔:缓存
两者的完整差异:
| 维度 | 301 Moved Permanently | 302 Found |
|---|---|---|
| 语义 | 资源永久换了地址,以后不应再访问原地址 | 本次临时去新地址,下次仍访问原地址 |
| 浏览器缓存 | 持久缓存这条跳转,直到用户清缓存 | 默认不缓存 |
| 可被缓存的条件 | 默认即可缓存 | 需显式 Cache-Control: max-age 才会缓存 |
| 搜索引擎 | 权重与排名转移到新地址 | 不转移,原地址权重保留 |
| 典型用途 | 换域名、www 与非 www 互跳、HTTP 升级 HTTPS、站点改版 | 未登录跳登录页、A/B 分流、临时维护页 |
| 客户端方法处理 | POST 可能被降级为 GET | 同样可能被降级为 GET |
1.4 301 缓存引发的排查陷阱
301 的缓存特性在真实运维里制造过大量"幽灵故障",值得单独记一笔。
场景:配置写错,把某路径 301 指到了一个 404 的地址。发现后改回配置,但访问者的浏览器依旧被跳到 404,而其他人访问正常。
原因:跳转已经被写进访问者的浏览器缓存,服务端改配置对已缓存的客户端无效。
这类故障的排查特征:
- 只有访问过的人受影响,新环境/无痕窗口正常
- 服务端日志显示该用户根本没发请求,因为请求被浏览器在本地拦下并直接重定向了
- 重启服务、改 nginx 配置、清 CDN 全部无效
对应的验证手段:
| 手段 | 用途 |
|---|---|
| 无痕窗口 | 排除本地缓存干扰 |
curl -I 且不加 -L | 看服务端返回的原始状态码,不受缓存影响 |
| 换一台机器 / 换网络 | 确认是否与特定客户端缓存绑定 |
检查响应头 Cache-Control | 判断该跳转是否具备被缓存的条件 |
结论:301 是一个会"留在客户端"的响应。 上线前对 301 的 Location 目标地址必须核对无误,因为它改错的成本比 302 高一个量级。
1.5 方法降级:POST 为什么会变成 GET
带请求体的 POST 遇到 301/302 时,历史上大量客户端(含浏览器)会把方法改成 GET 并丢弃请求体。RFC 对此是"允许"的,所以这不是实现 bug。
为了把"改方法"和"保方法"区分开,后续版本补了三个语义更严格的码:
| 状态码 | 方法与请求体 | 语义定位 |
|---|---|---|
| 301 / 302 | 可能被改成 GET,请求体可能被丢 | 兼容历史行为,规范允许改写 |
| 303 See Other | 强制改成 GET | 专门用于 POST 之后的跳转,即 PRG 模式 |
| 307 Temporary Redirect | 严格保持原方法与请求体 | 临时重定向的正确做法 |
| 308 Permanent Redirect | 严格保持原方法与请求体 | 永久重定向的正确做法 |
| 300 / 304 | 不是重定向 | 304 是协商缓存命中,容易被误归入重定向 |
1.6 选型规则
| 需求 | 用哪个 |
|---|---|
| 站点搬家、域名变更、HTTPS 升级 | 301 |
| 登录态失效跳登录页、灰度分流、临时公告 | 302 |
| POST 提交成功后跳到结果页 | 303(PRG 模式) |
| 接口重定向且必须保留方法、请求体 | 307 / 308 |
一句话记法:301 与 302 都会跳,差别只在这个跳转要不要被记住;要保住方法和请求体,必须换成 307/308。
二、502 与 504:网关替上游报的丧
2.1 判据只有一条:上游有没有回应
- 502 Bad Gateway:上游回了,但回应是废的(连接中途被 reset、报文头部不合法、头部超长、压根连不上)
- 504 Gateway Timeout:上游没回,网关计时到点,主动断开并报超时
一个容易记错的边界:连都连不上的情况属于 502,不是 504。 504 的成立前提是"连上了、请求发出去了、然后在等响应"这个阶段超时。
2.2 关键角色问题:502 和 504 是谁发的
这是最容易被忽略、但对排查影响最大的一点。
502 和 504 由网关(nginx、API Gateway、负载均衡器)发出,不是上游应用发出的。
上游应用自己报的错是 500 和 503,网关会把它们原样透传给客户端。因此:
| 看到的状态码 | 应该先查哪里 |
|---|---|
| 502 / 504 | 网关的 error.log,看 upstream 相关报错 |
| 500 / 503 | 应用自身的日志、进程状态、资源占用 |
把 502 当成"后端返回的错误码"去后端代码里找,是一个常见的排查方向性错误。上游对这次请求的结局根本不知情——它要么没收到,要么回了个网关看不懂的东西。
2.3 50x 末位含义
50x 统一表示"服务端侧的问题",末位数字才是具体语义。这张表比记忆谐音可靠:
| 状态码 | 名称 | 一句话 |
|---|---|---|
| 500 | Internal Server Error | 应用自己崩了,代码抛异常 |
| 501 | Not Implemented | 服务端不支持该方法,很少见到 |
| 502 | Bad Gateway | 网关从上游拿到的是无效回应,或连不上 |
| 503 | Service Unavailable | 应用明确表示当前不可用(过载、维护),常带 Retry-After |
| 504 | Gateway Timeout | 网关等到超时,上游没有出声 |
| 505 | HTTP Version Not Supported | 不支持的协议版本 |
对比着记会非常清晰:
- 500 → 应用自己承认出错(透传)
- 503 → 应用主动表明不接活(透传)
- 502 → 网关判定上游的回答无效(网关生成)
- 504 → 网关判定上游始终未响应(网关生成)
2.4 nginx 日志关键字对照
502 与 504 的 error.log 关键字,几乎可以直接反推根因:
| 日志关键字 | 真实原因 |
|---|---|
connect() failed (111: Connection refused) | 上游进程未启动、端口未监听 |
no live upstreams | 所有 upstream 节点探活失败 |
upstream prematurely closed connection | 上游提前关闭连接,keepalive 配置不匹配最常见 |
upstream sent invalid header | 上游返回的响应头不合法 |
upstream sent too big header | 响应头超过 buffer 限制 |
upstream timed out while connecting | 连接阶段就超时(注意:仍归入 502) |
upstream timed out while reading response header | 等响应头阶段超时,典型 504 |
前六条都指向 502,最后一条才指向 504。“timed out” 出现在 connecting 阶段和出现在 reading 阶段,对应的是两个不同的状态码,这一点极易混淆。
2.5 排查动作
| 现象 | 优先检查项 |
|---|---|
| 502 | 上游进程是否存活、端口是否监听(ss -lntp)、proxy_http_version 1.1 与 proxy_set_header Connection ""、响应头是否超 large_client_header_buffers、容器网络连通性 |
| 504 | proxy_read_timeout 与 proxy_connect_timeout 配置值、慢查询与索引、GC 停顿、线程池与连接池是否打满、外部第三方调用是否阻塞 |
2.6 两个高频误判
误判一:504 一定意味着上游故障。
proxy_read_timeout 默认值只有 60 秒。如果某个接口正常业务耗时 65 秒,网关会在 60 秒时准时掐表并报 504,而后端实际上一切正常。
看到 504,第一步应该是核对超时配置与正常耗时,而不是直接重启服务。这类"假故障"在生产环境里占比不低。
误判二:502 一定是后端挂了。
upstream sent too big header 会直接产生 502,但后端逻辑完全健康,只是 Set-Cookie 或自定义响应头太长,超过了 nginx 默认的 proxy_buffer_size(4k/8k)。
对应解法是调大 proxy_buffer_size 与 proxy_buffers,而不是去动后端。
三、PUT 为什么常被用作更新
3.1 唯一核心理由:幂等
这张图的关键在那个"超时"。
超时的时候,客户端无法区分请求没送到和请求送到了但响应丢了。 这是分布式系统的根本性不确定性,任何重试机制都绕不开。
幂等把这种不确定性变成无害:地址由客户端指定,内容完整给出,重发任意次资源状态都不变。非幂等的 POST 则无法预知上一次是否已创建成功,重发就会多出一份数据。
这不是规范洁癖。工程上到处都在依赖这个属性:
- nginx 的
proxy_next_upstream默认把 POST、LOCK、PATCH 排除在自动重试之外,因为它们非幂等;要显式配置non_idempotent才会转发到下一台上游 - 消息队列重投、SDK 自动重试、代理与 CDN 的失败重试,都按"该方法是否幂等"决定敢不敢重放
- 前端防重复提交、支付类接口的幂等键,本质上都是在给非幂等操作补一个幂等的外壳
3.2 幂等的严格定义
一个高频误解是把幂等理解成"返回值一样"。不是。
幂等指的是副作用状态相同,与返回值无关。
PUT /users/123 第一次创建成功返回 201 Created,第二次返回 200 OK 甚至 204 No Content,状态码不同,但数据库中那条记录完全一致——这依然是幂等。
反过来,如果一个接口每次都返回 200,但每次都在数据库里追加一条日志,它就不是幂等的。看的是副作用,不是响应。
3.3 PUT 的第二个好处:upsert
因为 URI 由客户端指定,PUT /users/123 天然就是"存在则替换、不存在则创建":
123不存在 → 创建,返回201 Created123已存在 → 整体替换,返回200或204
而 POST 需要两个接口(POST /users 创建、PATCH /users/123 更新),或者在一个接口里靠业务参数分流。少一套接口,就少一处上下文不一致。
3.4 PUT 是整体替换,不是部分更新
这是 PUT 最常被误用的一点。
PUT 的请求体被视为目标资源的完整新表示。 没有出现在请求体里的字段,按语义应当被清空或回落到默认值。
由此产生一个非常经典的生产事故:
用户表中密码、盐值等敏感字段不会返回给前端。前端拿到用户对象后改了一个昵称,再用 PUT 把整个对象发回服务端,密码字段因为不在请求体里而被写成 NULL 或空值——账号全部失效。
标准后来补了 PATCH(RFC 5789)来承载局部更新。三者的边界:
| 方法 | 语义 | 幂等 | 典型场景 |
|---|---|---|---|
| POST | 交给服务端处理,服务端决定结果与位置 | 否 | 创建、下单、支付、搜索 |
| PUT | 用请求体完整替换目标资源 | 是 | 整体覆盖配置、上传对象、幂等 upsert |
| PATCH | 只修改请求体中出现的字段 | 规范不要求 | 改昵称、改状态、局部字段更新 |
注意 PATCH 那一格:RFC 5789 明确不要求 PATCH 幂等,因为它支持 JSON Merge Patch 这类"增量操作"语义。这意味着 PATCH 的重试并不天然安全,需要业务层自行保证。
这也解释了为什么有些团队宁可坚持用 PUT(全量 + 幂等),也不用 PATCH——重试安全性在分布式环境里比"少传几个字段"更重要。
3.5 并发覆盖与 ETag
PUT 还有一个必须配套的东西:丢失更新(lost update)。
两个客户端同时 GET 同一个资源,各自修改,各自 PUT 回去,后到的那个会把先到的修改完整覆盖。全量替换放大了这个问题——被覆盖的不只是冲突字段,而是整个资源。
HTTP 的标准解法是乐观锁:
- 服务端在 GET 响应中返回
ETag: "v7" - 客户端 PUT 时带上
If-Match: "v7" - 服务端比对,版本不一致则返回
412 Precondition Failed
用 PUT 却不实现 ETag,等于把一次已知的并发 bug 留在系统里。 这是"用了 PUT 但没用对"的典型形态。
3.6 一个流行的误解:海外团队都偏爱 PUT
实际情况并不统一。主流 API 设计指南的取向差异很大:
| 来源 | 更新用什么 | 说明 |
|---|---|---|
| Google API 设计指南 | PATCH + FieldMask | 官方推荐 PATCH,PUT 只留给"客户端确实持有完整表示"的场景 |
| Kubernetes | PUT(全量)+ PATCH(strategic merge patch) | 另有 server-side apply 机制 |
| AWS S3 | PUT | PutObject 天然是整体覆盖,语义高度契合 |
| GitHub API | PATCH | 修改 issue、PR 均为 PATCH /repos/{owner}/{repo}/issues/{n} |
| Stripe | POST | 定位是可编程 API,并非严格 REST |
更准确的表述是:主流做法是按语义选方法,而不是偏爱某一个方法。 PUT 出现的频率高,是因为它同时满足幂等和 upsert 两个诉求,而不是因为某种习惯。
3.7 决策流程
三个判断问题:
- URI 谁定? 客户端能给出完整地址用 PUT,只能交给服务端生成用 POST
- 重复执行会不会出事? 会出事用 POST,不会用 PUT
- 传全量还是增量? 全量用 PUT,增量用 PATCH
3.8 国内 POST 一把梭的成因
"所有写操作都用 POST"并不是技术落后,有几条很实际的历史原因:
- HTML
<form>只支持 GET 与 POST,早期浏览器发不出 PUT - 框架支持不足:Spring 需要额外配
HiddenHttpMethodFilter从_method参数转换;PHP 没有$_PUT,必须手动读php://input - 把 PUT 误当成"部分更新"使用,一旦踩到字段被清空的坑,团队就整体退回 POST
- 业务系统里幂等性不显性(内网、低频、无自动重试),缺少推动力
这四条叠加起来,形成了"POST 一把梭"的工程惯性。它的代价在系统开始出现重试、网关重放、消息重投之后才会暴露。
四、速查与常见追问
4.1 三张表装下全部结论
重定向
| 状态码 | 会不会自动跳 | 会被缓存吗 | 保留方法吗 |
|---|---|---|---|
| 301 | 会 | 会,持久 | 否,可能降级为 GET |
| 302 | 会 | 默认不会 | 否,可能降级为 GET |
| 303 | 会 | 不会 | 强制改为 GET |
| 307 | 会 | 不会 | 是 |
| 308 | 会 | 会 | 是 |
5xx
| 状态码 | 谁发出的 | 判据 |
|---|---|---|
| 500 | 应用 | 应用自己崩了 |
| 502 | 网关 | 上游有回应但无效,或连不上 |
| 503 | 应用 | 应用明确拒绝服务 |
| 504 | 网关 | 上游在超时前没有任何回应 |
方法
| 方法 | 幂等 | 改的是全量还是增量 |
|---|---|---|
| POST | 否 | 服务端决定 |
| PUT | 是 | 全量替换 |
| PATCH | 规范不要求 | 增量 |
4.2 常见追问
Q1:浏览器看到 301 和 302,用户能感知差别吗?
不能。页面跳转结果一致,差别只在缓存与 SEO 权重,用户侧不可见。
Q2:curl 请求 302 只看到一段 HTML,是不是服务端没配好?
不是。curl 默认不跟随重定向,加 -L 即可看到最终结果。判断跳转行为要先确认客户端策略。
Q3:301 配错了改回来,为什么自己还是被跳走?
跳转已被写入浏览器缓存。服务端改动对已缓存客户端无效,需要清缓存或用无痕窗口验证。
Q4:502 和 504 会不会是同一种原因导致的?
可能同源但判据不同。后端进程挂掉通常给 502(连接被拒),后端卡死给 504(无响应)。同一个"后端不可用"的现象,会因为失败形态不同而落到两个状态码上。
Q5:504 但后端日志显示请求处理成功了,是什么情况?
典型超时错位。后端耗时超过网关 proxy_read_timeout,网关提前断开并报 504,而后端仍在继续执行并最终写库成功。这类场景会造成"用户看到失败、数据其实已写入",因此必须做幂等或状态查询补偿。
Q6:PATCH 既然不保证幂等,为什么还有人在用?
因为它能精确表达增量意图,配合 If-Match 做乐观锁后,重试安全问题可以用版本号解决,而不是靠方法本身的幂等性。
Q7:PUT 能否用来创建资源?
可以。客户端指定完整 URI(如 PUT /users/123),资源不存在时创建并返回 201 Created,存在时替换返回 200/204。这是标准的 upsert 语义。
Q8:GET 一定幂等吗?
语义上幂等,但实现上不保证。带副作用的 GET(如 GET /delete?id=1)在规范上是错误用法,且会被浏览器预取、CDN 缓存、爬虫意外触发。写操作不要用 GET。
Q9:为什么 nginx 不会自动重试 POST?
因为 POST 非幂等。proxy_next_upstream 默认只对幂等方法转发到下一台上游,避免重复写入。需要时必须显式加 non_idempotent,并自行保证业务幂等。
Q10:看到 502 时,第一步该做什么?
看网关的 error.log,按 2.4 节的关键字定位是"连不上"还是"回应无效"。前者查进程与端口,后者查响应头与 keepalive 配置。不要先去改后端业务代码。
五、收束
三条线归根到底在讲同一件事:HTTP 的每个状态码和方法都不是标签,而是一份对中间层做出的承诺。
- 301 承诺"这个跳转永久有效",于是客户端敢把它缓存下来
- 502 与 504 承诺"问题出在网关与上游之间",于是排查方向指向网关日志而非业务代码
- PUT 承诺"重复执行结果一致",于是负载均衡器与重试机制敢放心重放
读懂承诺,就不需要背;读不懂承诺,背下来也会在真实故障里用错方向。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2501_90355051/article/details/168662090




