入门指南
301 的「永久」没有期限:缓存自己估寿命,POST 还可能变成 GET
服务端改回来了,用户还停在旧地址:301 与 308 在 HTTP 缓存里默认可被启发式缓存,没有显式过期时间时由缓存自己估寿命;301 还允许把 POST 改成 GET。讲清四个状态码的缓存默认值与请求方法差异、换域名和改路径时怎么选,以及为什么旧跳转会留在客户端。
「永久」不等于「有期限」:缓存自己估的那个寿命,才是用户留在旧地址的时间。

先看答案
- 适合谁
- 刚把站点换到新域名、旧域名只留了一条 301 的人
- 改过 URL 结构、发现用户和搜索引擎还停在旧路径的人
- 正在决定某条跳转该用 301 还是 302 的人
- 你会获得
- 先问「这条跳转会不会写错」,再问「它要不要永久」。
- 发布时间
- 实质更新
- 发布
- AOPC 编辑部
60 秒结论
一分钟看懂这篇文章
「永久」不等于「有期限」:缓存自己估的那个寿命,才是用户留在旧地址的时间。
人负责
- 判断这条跳转该不该写成永久
- 算清改错之后的退出成本
AI 负责
- 逐条核对响应头与方法语义
- 用 curl 验证跳转链与缓存指令
- 分级跳转
- 小流量试
- 换状态码
- 再清理
直接答案
「换域名了,加一条 301 就行。」
这句话里藏着两个默认值:这条跳转会被缓存多久,以及它会不会动请求方法。两个默认值都不由你决定,除非你用响应头明确写下来。
| 状态码 | 语义 | 默认可被缓存吗 | 后续请求的方法 | 典型用途 |
|---|---|---|---|---|
| 301 | 永久移动 | 是(heuristically cacheable) | 可能从 POST 改成 GET(MAY) |
域名整体迁移、旧路径永久替换 |
| 302 | 临时找到 | 否,需显式标可缓存 | 可能从 POST 改成 GET(MAY) |
临时活动页、登录后回跳 |
| 303 | 去看另一个资源 | 否,需显式标可缓存 | 换成 GET/HEAD 去取 | 表单提交后跳结果页 |
| 307 | 临时重定向 | 否,需显式标可缓存 | 不变(MUST NOT change) |
临时跳转但要保住 POST |
| 308 | 永久重定向 | 是(heuristically cacheable) | 不允许改成 GET | 永久迁移且要保住 POST |
最容易被读错的是中间两列。「默认可被缓存」不是「缓存多久」——协议从头到尾没给过 301 的缓存时长;「可能改方法」不是「一定改」——原文用的是 MAY。下面按这两条线拆开。
一、「永久」在协议里只等于「默认允许缓存」
RFC 9110 给 301 的定义末尾只有一句关于缓存的话:
A 301 response is heuristically cacheable; i.e., unless otherwise indicated by the method definition or explicit cache controls (see Section 4.2.2 of [CACHING]).
308 的那句一字不差,只是把 301 换成了 308。这句里的关键词是 heuristically cacheable(可启发式缓存),不是「永久缓存」。
RFC 9110 §15.1 给了一份完整名单,可以逐字对照:
Responses with status codes that are defined as heuristically cacheable (e.g., 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, and 501 in this specification) can be reused by a cache with heuristic expiration unless otherwise indicated by the method definition or explicit cache controls [CACHING]; all other status codes are not heuristically cacheable.
这份名单里没有 302、303、307。三个 3xx 里有两个「永久」,两个「临时」都不在名单上——这不是笔误,是设计:临时跳转本来就该每次回源问一遍。
RFC 9111 §4.2.2 还交代了这个名字的来历:
Note that in previous specifications, heuristically cacheable response status codes were called “cacheable by default”.
换句话说,「默认可缓存」是旧名字,改叫「可启发式缓存」正是因为旧名字容易被读成「一定有缓存、一定有期限」。协议里没有任何一处给出 301 的缓存秒数——它只给出「允许缓存」和「缓存可以自己估寿命」两件事。
二、没有过期时间时,寿命是缓存自己估的
这是整篇最需要记住的一段。RFC 9111 §4.2.2 的开头:
Since origin servers do not always provide explicit expiration times, a cache MAY assign a heuristic expiration time when an explicit time is not specified, employing algorithms that use other field values (such as the Last-Modified time) to estimate a plausible expiration time. This specification does not provide specific algorithms, but it does impose worst-case constraints on their results.
三句话,三个后果:
MAY——缓存可以估,也可以不估。所以两台设备、两个浏览器、两个 CDN 节点看到的行为可以不同,而你这边一切正常。does not provide specific algorithms——规范不给算法。你无法从协议推出「301 会被记 30 天」这类数字,任何这样的说法都不是规范给的。worst-case constraints——规范只约束最坏情况,不约束常规表现。约束的方向是「别估太长」,不是「必须估多长」。
同一节还给了唯一一处比例提示:
If the response has a Last-Modified header field (Section 8.8.2 of [HTTP]), caches are encouraged to use a heuristic expiration value that is no more than some fraction of the interval since that time. A typical setting of this fraction might be 10%.
encouraged、some fraction、might be 10%——三个词都不是强制。但这句话给出了一个可推的方向:估出来的寿命与「这份内容多久没变过」挂钩(这是编辑推论,规范只给了比例建议,没给推导)。于是一个两年没改过的页面刚被改成 301,缓存估出来的寿命可能比一个昨天上线的页面长得多;而「刚发布就改错、马上改回来」的场景反而可能估得很短。这也解释了为什么同样的操作,在不同页面上表现不一样。
反过来,只要响应里有显式过期时间,启发式就不许用了:
A cache MUST NOT use heuristics to determine freshness when an explicit expiration time is present in the stored response.
这是唯一一个你能主动控制的开关:写不写 Cache-Control / Expires。第六节展开。
最后还有一条容易被忽略的余地。RFC 9111 §3 在列完「什么情况下缓存才可以存」之后补了一句:
Note that, in normal operation, some caches will not store a response that has neither a cache validator nor an explicit expiration time, as such responses are not usually useful to store. However, caches are not prohibited from storing such responses.
「有些缓存不会存」+「但也没禁止存」= 默认行为不可预测。这就是为什么同一个 301,有人第二天就恢复了,有人一个月还在旧地址,而两边都没有说谎。
三、301 与 302 都可能把 POST 变成 GET,307 与 308 不会
RFC 9110 在 301 的定义里放了一条带 Note 的例外:
For historical reasons, a user agent MAY change the request method from POST to GET for the subsequent request. If this behavior is undesired, the 308 (Permanent Redirect) status code can be used instead.
302 的定义里有一模一样的一条,只是把「改用 308」换成了「改用 307」。307 那条则是硬约束:
The 307 (Temporary Redirect) status code indicates that the target resource resides temporarily under a different URI and the user agent MUST NOT change the request method if it performs an automatic redirection to that URI.
308 的措辞在 RFC 7538 §3:
This status code is similar to 301 (Moved Permanently) ([RFC7231], Section 6.4.2), except that it does not allow changing the request method from POST to GET.
为什么会有这个历史包袱,RFC 9110 §15.4 的注解说得很清楚:
In HTTP/1.0, the status codes 301 (Moved Permanently) and 302 (Found) were originally defined as method-preserving ([HTTP/1.0], Section 9.3) to match their implementation at CERN; 303 (See Other) was defined for a redirection that changed its method to GET. However, early user agents split on whether to redirect POST requests as POST (according to then-current specification) or as GET (the safer alternative when redirected to a different site). Prevailing practice eventually converged on changing the method to GET. 307 (Temporary Redirect) and 308 (Permanent Redirect) [RFC7538] were later added to unambiguously indicate method-preserving redirects, and status codes 301 and 302 have been adjusted to allow a POST request to be redirected as GET.
翻译成操作语言:301 与 302 是「允许改」,307 与 308 是「不许改」。三条实践后果:
- 把一个 POST 端点挂在会 301 的旧域名上,等于让客户端自己决定要不要丢掉请求体。 支付回调、订阅 webhook、表单提交都属这一类。
- 具体改不改由客户端实现决定(
MAY),所以你测一台设备通过,不代表所有客户端都通过。 - 需要「永久 + 保住方法」时,唯一的选择是 308。而 308 本身有兼容成本,见第五节。
四、303 是「去看另一个资源」,不是搬家
303 常被当成「临时跳转」的同义词,但它的语义完全是另一件事:
A user agent can perform a retrieval request targeting that URI (a GET or HEAD request if using HTTP), which might also be redirected, and present the eventual result as an answer to the original request. Note that the new URI in the Location header field is not considered equivalent to the target URI.
两个要点:
- 它明确要求换成 GET/HEAD 去取——这不是「可能改」,是 303 的定义本身。
- 新 URI 与原 URI 不等价——303 说的是「答案在别处」,不是「资源搬到了别处」。
所以 303 的正当用途是「POST 之后跳到一个结果页」(Post/Redirect/Get):提交成功、刷新不会重复提交、结果页有自己的 URL 可以收藏。把它用在域名迁移上是错配:搜索引擎会把它当成两个不同资源。
五、为什么服务端改回来了,用户还在旧地址
「旧地址」至少有四个不同的存放位置,只有第一个属于 HTTP 缓存。
① HTTP 缓存(浏览器私有缓存 / CDN 等共享缓存)。 只要副本还是 fresh,缓存就直接拿它回答,根本不联系源站。RFC 9111 §2 把这两类分开定义:shared cache 是「给多个用户复用」的缓存,通常在中间层;private cache 是给单个用户的。
② 浏览器的历史机制(Back 按钮)。 RFC 9111 §6 专门写了一段:
For example, a history mechanism can display a previous representation even if it has expired, and an application can use cached data in other ways beyond its freshness lifetime.
并且明确:
The requirements in this specification do not necessarily apply to how applications use data after it is retrieved from an HTTP cache.
也就是说,「后退能看到旧页面」不违反 HTTP 缓存规范,它不是缓存没清干净,是另一套机制。
③ 应用层自己的缓存。 同一个 §6 还点了浏览器对图片等资源的页内缓存,以及应用自建缓存——这些「may or may not honor HTTP caching semantics」。Service worker、客户端 SDK、自动化脚本的本地状态都在这一类里。
④ 你在后台做的「保存」不会传播。 RFC 9111 §4.4 定义了失效(invalidation)的边界:不安全方法(PUT/POST/DELETE)成功后会失效目标 URI,可以顺带失效 Location 指向的 URI,但跨源的不得失效;最后一句是关键:
Note that this does not guarantee that all appropriate responses are invalidated globally; a state-changing request would only invalidate responses in the caches it travels through.
「只在它经过的那些缓存里」——你在自己浏览器里点保存,只清了自己这一条路径。
再补一句 RFC 9111 §4.2 的收尾,它能解释为什么「让用户刷新一下」有时没用:
Note that freshness applies only to cache operation; it cannot be used to force a user agent to refresh its display or reload a resource.
结论(编辑判断):规范里没有任何一条能让你「召回」已经发出去的 301。能够确定生效的只有时间——等到各层缓存自己判定的寿命结束。这句话听起来很丧,但它决定了下一节的全部设计。
六、止血:三条指令各自管什么、不管什么
先说清楚它们管的是未来:都是「这个响应该怎么被对待」,不是「把已经存下的删掉」。
| 指令 | 原文说了什么 | 能解决 | 不能解决 |
|---|---|---|---|
Cache-Control: max-age=0 |
响应在 age 大于 0 秒后即视为 stale | 让缓存每次用前回源确认 | 已按旧寿命存下的副本 |
Cache-Control: no-cache |
未经转发校验并拿到成功响应,MUST NOT 用它满足任何其他请求 |
禁止「不问就用」 | 同上;且离线时校验会失败 |
Cache-Control: no-store |
缓存 MUST NOT 存请求或响应的任何部分,私有多共享缓存都适用 |
让新响应根本不落盘 | 同上;它不定义「删除已存」 |
no-store 那条完整原文是:
The no-store response directive indicates that a cache MUST NOT store any part of either the immediate request or the response and MUST NOT use the response to satisfy any other request.
注意动词是 store。规范通篇没有给「远程删除某个缓存里的副本」的机制;能不能删、怎么删,取决于具体客户端或 CDN 产品(编辑判断)。所以:
- 还没被缓存过的响应:加显式指令立刻有效——因为只要有显式过期时间,启发式就
MUST NOT用了。 - 已经被缓存的副本:没有任何服务端指令能保证立刻失效。这条 301 会活到它自己算出来的寿命结束。
- CDN / 反向代理那一层:那是产品能力,不是协议能力,清缓存与核对另有一套流程(见文末链接的 CDN 篇)。
- 「让用户清一下浏览器缓存」:可以当个别用户的临时处置,不能当发布方案——它要求你联系到每一个用户,而历史机制(Back)那部分还不一定受清缓存影响。
这也顺带解释了最省事的那条策略:不确定的跳转,先别写成永久。
七、怎么选:五个场景
| 场景 | 选什么 | 理由 |
|---|---|---|
| 域名整体迁移,旧域名不再提供服务 | 301(要保住 POST 就用 308) | 语义就是「以后都用新地址」 |
只调整 URL 结构(/p/123 → /posts/123) |
301 | 同域内的永久替换,成本最低 |
| HTTP 全量跳 HTTPS | 301 | 与 HSTS 配合时另有一套前提,见 HSTS 篇 |
| 表单 POST 之后跳结果页 | 303 | 定义就是「换 GET 去取另一个资源」 |
| 临时活动页、维护页、灰度分流 | 302,需要保住方法就用 307 | 不在可启发式缓存名单里,默认每次回源 |
两条操作建议,都属编辑判断(规范没有对应的流程条款):
- 大迁移先用 302/307 跑一段时间。 这一步的目的不是「测试能不能跳」,而是「测试跳错之后能不能收回」。301 一旦被缓存,收回的手段就只剩等。
- 把「跳转」与「缓存时长」当成两个独立决定。 同一个 301 可以配
max-age=3600(一小时后回源确认一次),也可以配no-cache(每次都确认)。前者省流量,后者改起来快。这组数字是示例,不是推荐值。
八、十分钟核对
第一组:看跳转链本身
# 只看第一跳,不跟随
curl -sS -o /dev/null -D - --max-time 10 https://old.example.com/path
# 跟随到底,看最终状态码、最终 URL、跳了几跳
curl -sS -o /dev/null -L --max-time 10 \
-w 'final=%{http_code} url=%{url_effective} hops=%{num_redirects}\n' \
https://old.example.com/path
第一组看三件事:第一跳是不是你以为的状态码;Location 指向的是不是最终地址(而不是又一条跳转);hops 有没有异常变大(成环的信号)。
第二组:看有没有显式过期时间
curl -sSI --max-time 10 https://old.example.com/path \
| grep -i -e '^HTTP/' -e '^location:' -e '^cache-control:' -e '^expires:' -e '^last-modified:'
没有 Cache-Control 也没有 Expires,就等于把寿命交给缓存的启发式算法。 这是本节唯一必须记住的判据。
第三组:把四个入口一次扫完
for u in http://example.com/ http://www.example.com/ https://example.com/ https://www.example.com/; do
printf '%-30s ' "$u"
curl -sS -o /dev/null --max-time 10 -w '%{http_code} -> %{redirect_url}\n' "$u"
done
常见的漏配不是主域,是 www、裸域、以及旧域名下的深链接。四个入口指向四个不同目标,是很常见的事故形态。
第四组:确认 POST 打过去还在不在
对任何接收 POST 的旧地址(回调、webhook、表单 action),至少确认它不是 301/302。这一步只能看状态码,不能靠「我这边提交成功了」来证明——你自己的客户端可能恰好是保留方法的那一类实现。
判读表
| 你看到的现象 | 更可能的原因 | 下一步 |
|---|---|---|
第一跳 301,没有 Cache-Control / Expires |
寿命交给启发式算法 | 加显式指令;已缓存的只能等 |
有 Cache-Control: max-age=31536000 |
你自己把「一年」写死了 | 评估是否改成更短或 no-cache |
| 第一跳 302,但 CDN 上显示已缓存 | 共享缓存被显式标了可缓存 | 查 CDN 规则(属另一层) |
| POST 打到旧地址,落到 GET | 301/302 的 MAY 生效了 |
换 307(临时)或 308(永久) |
| 旧地址跳新地址又跳回旧地址 | Location 写反或成环 |
修 Location,别加缓存指令掩盖 |
| 浏览器 Back 回到旧页面 | 历史机制(§6),不是 HTTP 缓存 | 服务器侧无法修 |
| 用户清缓存后正常,别的用户仍旧 | 每台设备各自缓存、各自估寿命 | 只能等过期,或逐台处置 |
九、上线前十二项清单
- 每条跳转都写状态码与理由:为什么是永久、为什么不。
- 永久跳转必须配显式
Cache-Control,不要留空交给启发式。 - 确认跳转不是链:
Location直接指向最终地址,不要 A→B→C。 - 确认没有环:用第二组命令的
hops与url_effective交叉看。 - 四个入口都配齐:裸域、
www、http、https。 - 保留路径与查询串:深链接跳过去要落到同一个页面,而不是首页。
- 接收 POST 的地址不用 301/302;要临时且保方法用 307,要永久且保方法用 308。
- 308 上线前确认客户端能识别(见下方误读三)。
- 表单提交后用 303,不要用 301。
- 把「改回原状」的路径写下来:改哪个文件、谁有权上线、多久能生效。
- 大迁移先跑 302/307,观察足够长时间再换永久码。
- 记录改动日期:将来排查「为什么用户还在旧地址」时,这是唯一能倒推寿命起点的信息。
十、六种误读,与本周能做的五件事
误读一:「301 会被永久缓存。」
协议只说 heuristically cacheable,没有任何时长(§15.1、§15.4.2)。「永久」描述的是资源搬家这件事,不是缓存的有效期。
误读二:「301 一定把 POST 变成 GET。」
原文是 MAY。有的客户端改,有的不改;所以「我这边测试通过」不能证明线上安全。
误读三:「308 所有客户端都认识。」 RFC 7538 §4 写的是反面:
Section 6 of [RFC7231] requires recipients to treat unknown 3xx status codes the same way as status code 300 (Multiple Choices) ([RFC7231], Section 6.4.1). Thus, servers will not be able to rely on automatic redirection happening similar to status codes 301, 302, or 307.
RFC 9110 也提醒 308 is much younger (June 2014) than its sibling codes and thus might not be recognized everywhere。这里有一处规范之间的措辞差异:9110 §15.4 说 UA 可以在「不认识该状态码」时仍按 Location 跳转(MAY),而 7538 按当时的上位规范要求按 300 处理。两者不冲突——都是 MAY/兼容性口径,结论一致:不能依赖。这个对照属编辑判断。
误读四:「Location 是必需头。」
301/302/307/308 的原文都是 The server SHOULD generate a Location header field,UA 侧是 MAY use the Location field value for automatic redirection。没有 Location,跳转就不成立。
误读五:「清浏览器缓存就能解决。」 对单个用户可能有效;对「所有还停在旧地址的用户」无效——历史机制那一层不受 HTTP 缓存指令约束(§6),而你也联系不到所有人。
误读六:「no-store 能让已经发出去的 301 失效。」
它管的是「不许存」,不是「删掉已存」。规范没有定义远程召回机制。
本周能做的五件事:
- 列出所有 301/308,逐条看有没有
Cache-Control或Expires;没有的补上。 - 找出接收 POST 的旧地址(回调、webhook、表单 action),确认它们不是 301/302。
- 对主域、
www、裸域、http/https 四个入口跑一遍第三组命令,把不一致的记下来。 - 把一条准备上线的永久跳转先改成 302,跑够一段时间再换——给自己留一条退路。
- 写下「这条跳转是谁在什么时候加的」,写进仓库;下一次「用户还在旧地址」的排查会感谢你。
如果只能记住一件事:301 的成本不在加上的那天,而在改错的那天——那一刻你没有任何服务端手段能把已经发出去的「永久」收回,只能等每一层缓存自己算完那个没人告诉你的寿命。
边界说明:本文关于状态码语义与缓存行为的陈述全部来自 RFC 9110、RFC 9111 与 RFC 7538,三份来源均于 2026-10-31 访问并验证为 HTTP 200(字节数见「来源」)。不写「301 永久缓存」——规范只给 heuristically cacheable,没有时长;不写「301 一定把 POST 变 GET」——原文是 MAY;不写「308 所有客户端都支持」——RFC 7538 §4 明确不能依赖自动跳转;不写「Location 是必需头」——两侧分别是 SHOULD 与 MAY;不写「清浏览器缓存就能解决」——历史机制不受 HTTP 缓存指令约束;不写「no-store 能召回已发出的 301」——规范只约束存储,不定义远程删除。文中未给任何浏览器、CDN 或代理的默认缓存天数,也未给任何命中率、流量占比或事故时长数字。标注为编辑判断的有:第二节「寿命与 Last-Modified 间隔挂钩」的推论、第五节「规范没有召回手段」的结论、第六节「能否删除取决于具体产品」、第七节两条操作建议与示例数值、第十节误读三的规范对照。示例域名沿用 RFC 的保留示例域,不对应真实站点。本文只讲客户端与缓存如何对待重定向,不清边缘缓存(见《purge 返回 200 只说明请求收到了》一篇),也不重复 DNS 记录切换与 TTL 取证(见《改一条 A 记录不是发布》一篇)。
来源
以下页面均于 2026-10-31 逐条访问并验证为 HTTP 200;字节数为当日抓取的实际大小。
- RFC 9110: HTTP Semantics核查于 2026/10/31(200,502 941 B):§15.1 可启发式缓存的状态码名单(含 200、203、204、206、300、301、308、404、405、410、414、501)与
all other status codes are not heuristically cacheable;§15.4 3xx 分类、MAY automatically redirect ... even if the specific status code is not understood、自动跟随时的请求改写五条、SHOULD detect and intervene in cyclical redirections,以及 301/302 方法变更历史的长注;§15.4.2 的 301 定义、SHOULD generate a Location header field、MAY use the Location field value、POST→GET 的MAY注记与heuristically cacheable;§15.4.3 的 302 定义与同样的方法注记;§15.4.4 的 303 定义、a GET or HEAD request if using HTTP、the new URI ... is not considered equivalent to the target URI;§15.4.8 的 307MUST NOT change the request method;§15.4.9 的 308 定义、heuristically cacheable、much younger (June 2014)注记;§10.2.2 的Location语法、相对引用解析与 fragment 继承。 - RFC 9111: HTTP Caching核查于 2026/10/31(200,84 477 B):§2 的
shared cache与private cache定义;§3 存储条件清单(含「状态码被定义为可启发式缓存」这一条)与some caches will not store a response that has neither a cache validator nor an explicit expiration time ... However, caches are not prohibited from storing such responses;§4.2 fresh/stale 定义、显式过期时间优先、caches are not required to honor them、freshness applies only to cache operation; it cannot be used to force a user agent to refresh its display or reload a resource;§4.2.2 启发式新鲜度的MAY、does not provide specific algorithms、worst-case constraints、有显式过期时间时MUST NOT用启发式、no more than some fraction of the interval since that time与A typical setting of this fraction might be 10%、旧名cacheable by default;§4.4 失效只覆盖目标 URI 与同源Location、would only invalidate responses in the caches it travels through;§5.2.2.1max-age语义;§5.2.2.4no-cache的MUST NOT be used ... without forwarding it for validation and receiving a successful response;§5.2.2.5no-store的MUST NOT store any part of either the immediate request or the response、对私有多共享缓存同样适用;§6 历史机制可以显示已过期表示、The requirements in this specification do not necessarily apply to how applications use data after it is retrieved from an HTTP cache。 - RFC 7538: The Hypertext Transfer Protocol Status Code 308 (Permanent Redirect)核查于 2026/10/31(200,11 189 B):§3 的 308 定义、
a 308 response is cacheable by default、does not allow changing the request method from POST to GET;§4 部署注意事项——不认识的 3xx 按 300 处理、servers will not be able to rely on automatic redirection happening similar to status codes 301, 302, or 307、建议不要按User-Agent变化状态码、<meta>refresh 兜底示例。