资讯动态

301/302/303/304:HTTP重定向与协商缓存实战解析

发布时间:2026/10/27 23:25:58 来源:尧图企业网站定制
面试里问到 301 和 302 的区别十个人有八个能脱口而出“永久和临时”紧接着把话题拐到 303、304能说清楚的就剩两三个了。网络状态码 301、302、303、304 这四个看起来都是三位数实际分属两套完全不同的机制前三个是重定向家族负责把请求“支”到另一个地址304 压根不是重定向它是协商缓存的产物。我在带新人和做技术面时反复见过同一个问题——大家把四个码背成了一张表却说不清浏览器拿到响应之后到底做了什么于是线上出了重定向死循环、POST 重复提交、发版后用户拿不到新资源这些事故排查时反而绕远路。这篇东西不讲教科书定义我想按“面试官真正想听什么”的顺序把它们拆开顺带把每个状态码在真实项目里的用法、配置写法、踩坑点都放进去。前端同学看完能应付追问后端同学看完能少写几个错误的 redirect做运维和 SEO 的同学也能对上号。读完你至少能做到三件事看到状态码就知道该查哪一层、写重定向时不再随手一个 302 了事、给接口加缓存时知道 304 该由谁来触发。1. 先把四个状态码放进同一张地图1.1 它们各自属于哪个家族很多人答错的第一步是把 304 当成“重定向的第四种”。这属于分类错误后面越答越乱。正确的划分是3xx 是一个大类叫 Redirection重定向但它内部还分“真的换了地址”和“没换地址只是告诉你别重复传数据”两拨。301 / 302 / 303 / 307 / 308地址变了或者临时变了客户端需要重新发一次请求到Location指定的地址。304 / 300 / 305语义完全不同。300 是多种选择305 早就废弃了304 则是“你本地那份还能用别再来问我要完整内容了”。所以 304 虽然挂在 3xx 里行为上更接近 2xx——它是一次成功的协商只是没有响应体。我在面试时特别爱问一句“304 算成功还是失败”答“成功”的人我会追问为什么答“失败”的人基本就暴露了没读过规范。1.2 一句话记住核心差异如果面试只剩三十秒我会这样压缩状态码核心语义请求方法会变吗默认是否可缓存典型用途301永久搬家规范上不该变历史上会被改成 GET是且缓存时间长域名迁移、URL 结构永久调整302临时指路规范上不该变浏览器历史上会改成 GET默认不缓存除非显式声明登录后跳首页、活动页临时导流303去看另一个地址强制变成 GET一般不缓存表单提交后的 PRG 跳转304内容没变用你本地的不变本来就是条件请求本身不缓存但会刷新本地缓存有效期协商缓存、静态资源复用这张表建议直接记下来但别只记表。面试官追问的一定是“为什么”——为什么 302 会有方法被改写的黑历史、为什么 303 要单独存在、为什么 304 返回了却没有 body。下面几节逐个说。1.3 一个容易忽略的前置知识重定向是客户端行为新手常见的误解是“服务器帮我跳过去了”。不是的。服务器做的事情只有一件返回一个 3xx 状态码 一个Location响应头然后结束这次响应。真正发起第二次请求的是浏览器、爬虫、或者你用的 HTTP 客户端。这个认知很关键它直接解释了三件事为什么curl默认不跟随重定向——那是客户端的选择得加-L。为什么 301 一旦被浏览器记住你在服务器上改配置也没用——缓存存在客户端那边。为什么用fetch拿到 301 时response.status有时根本看不到 3xx——浏览器在redirect: follow模式下替你走完了全程最终给你的是终点响应。我在排查一个“接口偶发 404”的问题时卡了很久最后发现是测试同学用 Postman 关了自动重定向而浏览器开着两边看到的响应根本不是一个层级的东西。调接口时先确认客户端跟不跟随重定向能省掉大量自我怀疑。2. 301为什么它是一份“不可逆”的承诺2.1 语义层面的永久和缓存层面的永久301 的全称是 Moved Permanently字面意思就是“这个资源以后永远不在这了”。规范对它的描述里藏着一个致命细节响应默认是可缓存的cacheable by default。什么叫“默认可缓存”意思是浏览器、中间代理服务器、CDN 在没有收到任何Cache-Control指示的情况下也有权把它存下来并且在一段时间内对同一个 URL 的后续请求直接返回缓存的 301连源站都不问一声。这就造成了那个经典事故某内容站做 URL 改版把/article/123用 301 指到/posts/123上线三天后发现规则写错了想回滚。改服务器配置很快但用户端不认——他们的浏览器里已经存了 301访问旧地址依然自动跳到新地址甚至有些用户几个月后还是这样。这就是“服务器改了、客户端不改”的典型场景。注意不要用 301 做任何你可能反悔的跳转。灰度测试、临时活动、A/B 分流一律用 302 或 307。判断标准很简单——这个跳转规则你愿意让它生效一年以上吗2.2 搜索引擎视角301 是权重传递的通道做前端可能感受不深但做过站点迁移的同学应该知道301 是搜索引擎判断“旧 URL 的权重该不该传给新 URL”的主要信号。用 301 做域名切换、URL 规范化、http 到 https 的升级是行业内的标准动作。反过来如果你用 302 做永久迁移搜索引擎会认为旧地址只是暂时指路权重仍然挂在旧 URL 上新页面迟迟起不来。我在一次站点改版里见过真实的对比同一批 2000 个页面一半用 301、一半误用了 302三个月后 301 那批的收录和流量基本平移过去了302 那批还在新旧地址之间反复横跳抓取频次也低。这个差异不是玄学是搜索引擎对状态码语义的直接采信。顺带说一句301 对旧 URL 的“清理”也有帮助——持续返回 301 一段时间之后旧地址会慢慢从索引里退出去而 302 会让新旧两个地址长期共存反而稀释权重。2.3 该用和不该用的清单用 301 的合理场景网站从http://全量升级到https://或者是裸域跳www。站点整体更换域名旧的域名不再维护内容。URL 结构永久调整比如/p/123改成/article/123且老结构明确废弃。页面的规范地址统一把带参数的重复 URL 收敛到唯一地址。不该用 301 的场景登录成功跳首页、下单成功跳结果页。这类跳转跟着用户操作走天天在变用 302 或 303。短期活动页导流。任何可能回滚的跳转规则尤其是还在灰度观察期的。2.4 三种常见技术栈的写法写重定向最容易出问题的地方在于很多框架的默认状态码是 302你不显式指定就会用错。Express 里res.redirect()不传状态码时默认 302// 错误示范想永久迁移却写成了临时跳转 app.get(/old-path, (req, res) { res.redirect(/new-path); }); // 正确写法显式指定 301 app.get(/old-path, (req, res) { res.redirect(301, /new-path); });Nginx 的rewrite指令用permanent才是 301用redirect是 302这个单词很容易记混# 301 永久跳转 rewrite ^/old-path$ /new-path permanent; # 等价写法return 更高效推荐 location /old-path { return 301 /new-path; }Koa 则是先设状态码再调用redirectrouter.get(/old-path, (ctx) { ctx.status 301; ctx.redirect(/new-path); });实操心得return比rewrite高效因为rewrite会触发一次重新匹配 location 的流程而return直接结束处理。跳转规则多的站点全部换成return能省下一点不必要的内部开销。3. 302最容易用错、也最有故事的一个3.1 临时跳转的正确定位302 叫 Found早年叫 Moved Temporarily改名的原因很实在——它描述的其实不是“资源临时搬到另一个地址”而是“你要找的东西这次在这个地址找到了”。语义上非常弱弱到规范里只说了两件事Location指向新地址以及客户端不应该把它当成永久状态缓存。正因为“临时”302 在默认情况下不可缓存除非你显式加Cache-Control。这一点和 301 正好相反也是它适合登录跳转、活动导流、按用户状态分流的原因。用户下次再访问原地址还会老老实实打到服务器你随时能改规则。3.2 那段被写进规范的历史公案这是面试高频追问点也是我认为最能看出一个人有没有真正读过 HTTP 演进的地方。HTTP/1.0 时代302 的规范文字写得很含糊客户端收到 302 后应该用同样的方法去请求Location指向的地址。听起来没问题但当时的浏览器实现集体跑偏了——它们发现这个规则用起来很别扭于是自作主张不管原来是什么方法一律改成 GET。结果就是你今天还能见到的经典现象一个表单用 POST 提交到/submit服务端返回 302 跳到/thanks浏览器第二次请求就变成了 GET/thanks。绝大多数情况下这没什么问题甚至挺方便。但有些场景会直接翻车有些接口要求 POST 才能访问跳转后变成 GET直接 405。有些敏感数据原本在 body 里改成 GET 后被拼进 URL进了日志和浏览器历史。更麻烦的是这种改写让“重定向后请求方法可预期”这件事彻底失去了保证。HTTP/1.1 修订时规范组做了两件事一是把 302 的措辞改得更贴近现实二是新增了307 Temporary Redirect明确写死“请求方法不得改变”。后来 308 也是同样的思路对应 301 的“永久 不改方法”版本。所以四个码之外你最好顺便记住这两个状态码语义方法是否保持缓存301永久规范要求保持现实中可能被改成 GET默认可缓存302临时规范要求保持浏览器历史上会改成 GET默认不缓存303看别处强制改成 GET一般不可缓存307临时严格保持原方法默认不缓存308永久严格保持原方法默认可缓存提示只要你的重定向发生在 POST 之后并且你不希望方法被改写就用 307临时或 308永久别用 302。这条规则能避开绝大多数重定向相关的方法丢失问题。3.3 一个真实的排查片段去年有个同学问我说他们后台的导出功能偶发失败前端拿到的是 405。我让他把网络面板的“保留日志”打开重新操作一次看到了完整链条POST /api/export 302 GET /api/export/result 405原来服务端处理完导出请求后用res.redirect(/api/export/result)让前端去取结果而这个/api/export/result接口只接受 POST。浏览器按历史行为把第二次请求改成了 GET于是 405。改法很简单把res.redirect()换成显式 307app.post(/api/export, async (req, res) { const taskId await createExportTask(req.body); // 用 307 保持 POST 方法不变 res.redirect(307, /api/export/result?taskId${taskId}); });不过他后来改成不重定向了——直接返回任务 ID让前端自己轮询。很多时候重定向不是唯一解尤其是接口之间多一次往返就多一次出错的机会。4. 303为什么它必须存在4.1 PRG 模式POST 之后为什么要跳一下先看一个没有 303 的世界会发生什么。用户填完订单表单浏览器 POST 到/order/create服务端返回 200 并渲染“下单成功”页面。此时用户按一下 F5 刷新浏览器会弹窗问“是否重新提交表单”用户手一抖点了确定——订单被创建了两次。这个问题的解法叫Post/Redirect/Get简称 PRGPOST 处理完之后不要让服务端直接返回页面而是返回一个 3xx 让浏览器再发一次 GET 去拿结果页。这样刷新的时候浏览器重复的是最后那个 GET不会再触发 POST。PRG 里的那个 3xx最标准的选择就是 303 See Other。它的语义非常干脆用 GET 方法去访问Location指向的地址。注意是“用 GET”不是“保持原方法”不是“可能改成 GET”。303 是四个码里唯一一个在语义层面就要求换方法的。4.2 303 和 302 的实际差别在哪很多同学会问既然浏览器收到 302 也会改成 GET那我用 302 实现 PRG 不行吗行而且在很长一段时间里大家都是这么干的。但 302 的“改成 GET”是浏览器擅长的历史行为不是规范承诺而 303 的“用 GET”是写进规范的硬要求。两者的区别在于302规范期望保持方法行为上不一致属于历史遗留的妥协。303规范明确规定换 GET适用于“处理结果不在这个地址上去另一个地址看”的场景。举个对照例子。假设有个/logout接口用 GET 是会有安全风险的预加载、爬虫可能误触发所以设计成 POST。用户点退出按钮前端 POST/logout服务端清理会话后应该跳登录页。这里用 303 是最贴切的动作已经执行完接下来去看登录页用 GET 拿。app.post(/logout, (req, res) { req.session.destroy(() { // 操作完成引导客户端用 GET 去看登录页 res.redirect(303, /login); }); });而如果是对一个静态资源做临时分流比如灰度期间把一部分用户导到备用地址那就该用 302 或 307因为那是“换地址”不是“做完事了去看结果”。我的判断口诀“做完一个动作然后去看某页”——用 303“这个地址现在暂时不在我这”——用 302/307。前者关注的是动作已经完成后者关注的是地址变更。4.3 前端代码里怎么识别重定向用fetch的时候redirect有三个取值默认是follow// 默认行为浏览器自动跟随你拿到的是终点响应 const res await fetch(/api/action, { method: POST, redirect: follow }); console.log(res.status); // 通常看不到 3xx // 想看中间过程就把跟随关掉 const raw await fetch(/api/action, { method: POST, redirect: manual }); console.log(raw.type, raw.status); // opaqueredirect状态码拿不到这里有个新手常踩的坑redirect: manual在浏览器里拿到的是一个opaqueredirect类型的响应status是 0headers也读不到。这是为了防跨域信息泄露做的限制。想真正观察重定向链条最靠谱的办法还是开发者工具的 Network 面板或者用curl。5. 304唯一一个“没有内容”的成功响应5.1 协商缓存到底在协商什么304 的触发前提是条件请求。浏览器在第一次拿到资源时服务端会给两个东西里的一个或两个Last-Modified这个文件最后一次修改的时间。ETag这个文件内容的一个指纹哈希值或者基于版本号的字符串。第二次请求同一个资源时浏览器会带上对应的条件头If-Modified-Since上次拿到的修改时间。If-None-Match上次拿到的 ETag。服务端比对一下如果没变返回 304不带响应体如果变了返回 200 加新内容。浏览器收到 304 就知道“我本地那份还能用”直接用缓存渲染同时在 Network 面板里显示为from disk cache或from memory cache之类的标记。// 服务端实现协商缓存的简化示例 app.get(/static/data.json, (req, res) { const content fs.readFileSync(./data.json); const etag ${createHash(md5).update(content).digest(hex)}; // 关键一步比对客户端带来的 ETag if (req.headers[if-none-match] etag) { res.status(304).end(); // 注意304 不能带 body return; } res.set(ETag, etag); res.set(Cache-Control, no-cache); // 可缓存但每次都要校验 res.type(json).send(content); });这段代码里有两个容易写错的地方。第一res.status(304).end()不能传内容规范里明确 304 响应不能有消息体。第二Cache-Control要设成no-cache而不是no-store——no-cache的意思是“可以存但用之前必须问源站”正好对应协商缓存no-store是“完全不许存”那就永远不会有 304 了。5.2 ETag 和 Last-Modified 该怎么选对比项Last-ModifiedETag精度秒级一秒内多次修改会漏判内容级改一个字节就会变生成成本几乎为零文件系统自带需要计算哈希或维护版本号代理支持好好但弱 ETag 在代理链路上可能被改写兼容性全部支持全部支持常见问题内容没变但修改时间变了导致误判为已更新集群环境下多台机器生成的 ETag 不一致导致缓存失效生产环境里我的习惯是两个都给让浏览器优先用 ETag。原因是Last-Modified有一个天然缺陷如果文件被回滚修改时间会变晚但内容其实和某个旧版本一样服务端仍然会返回 200 走一遍全量传输。ETag 就没这个问题内容一样就是一样。但如果你的服务是多机部署而且 ETag 是基于文件mtime或进程内状态生成的就会出现“浏览器带过来的 ETag 打到的机器不认识”的情况结果 304 永远命不中。这种“缓存看起来配了但就是不生效”的问题八成是多节点 ETag 不一致导致的。解决办法是把 ETag 的生成逻辑收敛成基于内容哈希或者干脆交给 CDN 统一处理。5.3 304 实战里最常遇到的三个异常异常一改了文件用户刷新还是旧内容。先确认缓存层级。如果响应头是强缓存Cache-Control: max-age31536000且文件带哈希指纹那这是预期行为——文件名变了才会更新。如果文件名没变却更新不了多半是你把max-age设得很长又没有版本号。正确做法是HTML 入口用短缓存或no-cache静态资源用长缓存 文件名哈希。异常二Network 面板里一堆 304但页面加载还是很慢。304 虽然省了响应体但请求本身没有省。一个页面上百个静态资源每个都发一次条件请求再收一个 304往返延迟累加起来依然可观。这种情况下该上的是强缓存让浏览器连请求都不发。协商缓存解决的是“内容复用”不是“请求次数”。异常三接口返回 304前端拿不到数据。这是纯粹的设计问题。304 不能带响应体如果你的接口在某些客户端下把 304 当成“空响应”业务代码会直接解析失败。给 JSON 接口配协商缓存要慎重尤其是会被fetch和各类客户端调用的接口。动态数据更适合用短时间的max-age或者干脆不缓存。实操心得调试缓存问题时DevTools 的 Network 面板一定要取消勾选 “Disable cache”。这个选项开着的时候浏览器会绕过所有缓存直接请求你会看到全是 200误以为缓存没生效。我见过不止一个人在这上面浪费半天。6. 面试现场怎么把这四个码讲出层次6.1 一个能撑三分钟的答题框架被问到“说说这几个状态码的区别”别上来就是流水账。我建议按这个顺序展开第一步先分类。一句话点明 301/302/303 是重定向304 是协商缓存不属于同一个机制。这一步能立刻把你和背题库的人区分开。第二步讲重定向的三个维度。是不是永久、方法会不会变、能不能被缓存。把 301 和 302 按前两个维度对比把 303 单独拎出来说“它是唯一强制换 GET 的”顺便带一句 307/308 是 302/301 的方法安全版本。第三步讲 304 的触发条件。强调它是条件请求的产物依赖If-None-Match/If-Modified-Since响应没有 body本质是省流量不是省请求。第四步落到场景。各举一个你实际做过的例子域名升级用 301、登录跳转用 302、表单提交后用 303、静态资源用 304。有场景的答案可信度完全不一样。6.2 高频追问与应答要点追问应答核心301 用了之后能撤销吗服务端能改但客户端缓存的 301 不受你控制可能持续很久所以别对可能回滚的规则用 301302 和 307 到底差在哪差在方法是否保持。302 现实中会被改成 GET307 规范上严格保持POST 场景优先 307为什么 304 没有响应体语义就是“内容没变”传 body 等于重复传输规范禁止ETag 和 Last-Modified 一起发会怎样服务端优先校验 ETag两者都满足才算未修改实现上以 ETag 为准更可靠重定向会不会影响性能每次重定向都是一次额外往返移动网络下几十到几百毫秒链式重定向更糟重定向有次数限制吗浏览器一般限制在 20 次左右超过报ERR_TOO_MANY_REDIRECTS最后那条我强烈建议记一下因为重定向死循环是线上最常见的 3xx 事故。典型成因是规则写得过于宽泛比如把所有非 https 请求 301 到 https但健康检查走的是 http 且只认 200于是检查失败、流量被摘、服务“假死”。排查时看 Network 面板里同一个 URL 反复出现、状态码一路 301基本就能定位。7. 常见问题与排查技巧实录7.1 一张能直接贴到排查清单里的速查表现象最可能的原因排查动作改了跳转规则部分用户还是老行为301 被浏览器缓存让用户清缓存验证规则本身改用 302POST 之后接口报 405302 把方法改成了 GETNetwork 面板看第二次请求的方法改用 307刷新页面提示重复提交表单没做 PRG提交后返回 303 跳结果页静态资源更新后用户仍是旧版强缓存时间过长且文件名没变改成文件名带哈希入口 HTML 用 no-cache304 一直命中不了ETag 多节点不一致 / 请求没带条件头对比响应头的 ETag 与请求头的 If-None-Match页面报 ERR_TOO_MANY_REDIRECTS重定向规则互相指向逐跳看 Location检查是否有环接口返回 304 但前端报解析错误业务代码没处理空响应接口层去掉协商缓存或前端加状态判断7.2 三个我常用的排查动作动作一用 curl 看原始响应绕开浏览器的所有自动化行为。# 只看响应头不跟随重定向能看到最原始的 3xx 和 Location curl -I http://example.com/old-path # 跟随重定向并打印每一跳的详情 curl -IL http://example.com/old-path # 带条件头模拟协商缓存 curl -I -H If-None-Match: abc123 http://example.com/static/app.js-I只发 HEAD 请求拿响应头-L才跟随。这两个参数组合起来重定向链条一目了然。我排查线上问题时第一步永远是curl -IL比在浏览器里点来点去看 Location 快得多。动作二Network 面板打开“保留日志”再操作。重定向会把前一条请求的记录冲掉不勾选保留日志你只能看到最后一跳。勾上之后整条链路的每一跳、每一跳的方法和状态码都留着重定向死循环在这个视图下一眼可见。动作三确认是不是 CDN 在中间做了手脚。很多 CDN 默认会把源站的 302 改成 301或者反过来。我遇到过一次源站返回 302用户端收到的却是 301查了半天最后发现是边缘节点的缓存策略覆盖了状态码。只要链路里有一层代理就必须在代理层也确认一遍状态码有没有被改写。排查方法是直接打源站 IP 对比或者在响应头里找X-Cache之类的标记。7.3 几条不太写在文档里的经验经验一跳转规则别超过两跳。每多一跳就多一次往返移动端尤其明显。做 URL 迁移时我习惯直接一步到位从旧地址直接 301 到最终地址不要 A 跳 B、B 再跳 C。经验二域名级跳转放在最外层别混在业务路由里。用 Nginx 的 server 块统一处理 http 到 https、裸域到 www业务代码里就不要再写这类跳转否则规则分散在多处出问题很难找。经验三给 301 加个监控。统计一下每天有多少请求打到了本该 301 的旧地址如果数量长期不下降说明有大量客户端还在用旧链接这时候急着下线旧地址就是给自己找麻烦。经验四接口层的重定向能不用就不用。前端调接口最怕中间夹一个 3xx各种 HTTP 客户端对重定向的处理策略还不一致很容易出现“浏览器好使、App 里不行”的问题。接口返回数据让调用方自己决定去哪比服务端替你跳要稳妥得多。经验五测试环境一定要把缓存关掉验证。我见过太多“测试环境好好的上线就出问题”的案例根因都是测试时禁用了缓存生产环境缓存一开行为完全不同。发版前至少用真实缓存策略跑一遍完整流程尤其是带重定向和协商缓存的路径。这四个状态码本身不难难的是把它们和真实系统里的行为对上号。我自己踩过的坑一半来自“以为服务器改了就行”另一半来自“以为浏览器会按规范来”。把这两条假设放下遇到 3xx 先问一句“这次跳转是谁在跳、跳完客户端会记住什么”大部分问题都能顺着线索摸到根上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑