一只旭宝头像
【网络精讲4】HTTP 状态码与方法语义:301 与 302 的缓存差异、502 与 504 的判据、PUT 幂等与 PATCH 边界封面图

【网络精讲4】HTTP 状态码与方法语义:301 与 302 的缓存差异、502 与 504 的判据、PUT 幂等与 PATCH 边界

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 真正的分岔:缓存

本地缓存新地址原地址客户端本地缓存新地址原地址客户端后续请求 /old 直接命中缓存默认不写入缓存alt[301 Moved Permanently][302 Found]请求 /old1301 响应,Location 指向 /new2写入永久跳转记录3302 响应,Location 指向 /new4自动请求 /new5200 OK6

两者的完整差异:

维度301 Moved Permanently302 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。

服务端客户端服务端客户端多数实现改为 GET,并丢弃请求体alt[响应为 302][响应为 307]POST /submit,携带请求体1302 响应,Location 指向 /result2GET /result,无请求体3307 响应,Location 指向 /result4POST /result,请求体原样带上5200 OK6

为了把"改方法"和"保方法"区分开,后续版本补了三个语义更严格的码:

状态码方法与请求体语义定位
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

504 Gateway Timeout

  • 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 统一表示"服务端侧的问题",末位数字才是具体语义。这张表比记忆谐音可靠:

状态码名称一句话
500Internal Server Error应用自己崩了,代码抛异常
501Not Implemented服务端不支持该方法,很少见到
502Bad Gateway网关从上游拿到的是无效回应,或连不上
503Service Unavailable应用明确表示当前不可用(过载、维护),常带 Retry-After
504Gateway Timeout网关等到超时,上游没有出声
505HTTP 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、容器网络连通性
504proxy_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 唯一核心理由:幂等

服务端客户端服务端客户端无法区分「请求未送达」与「响应丢失」alt[幂等方法 PUT /users/123][非幂等方法 POST /users]写请求,随后响应超时1重发同一请求2数据库中仍为一条记录3重发同一请求4数据库中产生第二条记录5

这张图的关键在那个"超时"。

超时的时候,客户端无法区分请求没送到和请求送到了但响应丢了。 这是分布式系统的根本性不确定性,任何重试机制都绕不开。

幂等把这种不确定性变成无害:地址由客户端指定,内容完整给出,重发任意次资源状态都不变。非幂等的 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 Created
  • 123 已存在 → 整体替换,返回 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 的标准解法是乐观锁:

  1. 服务端在 GET 响应中返回 ETag: "v7"
  2. 客户端 PUT 时带上 If-Match: "v7"
  3. 服务端比对,版本不一致则返回 412 Precondition Failed

仍为 v7

已被改为 v8

客户端 GET /users/123

响应携带 ETag: v7

客户端修改后 PUT,携带 If-Match: v7

服务端比对当前版本

接受更新,返回新 ETag v8

返回 412 Precondition Failed

客户端重新读取后重试

用 PUT 却不实现 ETag,等于把一次已知的并发 bug 留在系统里。 这是"用了 PUT 但没用对"的典型形态。

3.6 一个流行的误解:海外团队都偏爱 PUT

实际情况并不统一。主流 API 设计指南的取向差异很大:

来源更新用什么说明
Google API 设计指南PATCH + FieldMask官方推荐 PATCH,PUT 只留给"客户端确实持有完整表示"的场景
KubernetesPUT(全量)+ PATCH(strategic merge patch)另有 server-side apply 机制
AWS S3PUTPutObject 天然是整体覆盖,语义高度契合
GitHub APIPATCH修改 issue、PR 均为 PATCH /repos/{owner}/{repo}/issues/{n}
StripePOST定位是可编程 API,并非严格 REST

更准确的表述是:主流做法是按语义选方法,而不是偏爱某一个方法。 PUT 出现的频率高,是因为它同时满足幂等和 upsert 两个诉求,而不是因为某种习惯。

3.7 决策流程

服务端生成

客户端指定

完整表示

仅变更字段

需要修改服务端资源

完整 URI 由谁决定

POST
不幂等,重试需幂等键去重

提交的是全量还是增量

PUT
整体替换,幂等

PATCH
局部更新,规范不要求幂等

三个判断问题:

  1. URI 谁定? 客户端能给出完整地址用 PUT,只能交给服务端生成用 POST
  2. 重复执行会不会出事? 会出事用 POST,不会用 PUT
  3. 传全量还是增量? 全量用 PUT,增量用 PATCH

3.8 国内 POST 一把梭的成因

"所有写操作都用 POST"并不是技术落后,有几条很实际的历史原因:

  1. HTML <form> 只支持 GET 与 POST,早期浏览器发不出 PUT
  2. 框架支持不足:Spring 需要额外配 HiddenHttpMethodFilter 从 _method 参数转换;PHP 没有 $_PUT,必须手动读 php://input
  3. 把 PUT 误当成"部分更新"使用,一旦踩到字段被清空的坑,团队就整体退回 POST
  4. 业务系统里幂等性不显性(内网、低频、无自动重试),缺少推动力

这四条叠加起来,形成了"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

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--