
接口文档丢过来往往只有一行 URL 和几个字段名剩下的全靠猜猜对方要什么Content-Type猜失败时返回 400 还是 422猜缓存到底会不会命中猜一个PUT重发两次会不会出问题。这些问题的答案几乎全部藏在 HTTP 里。超文本传输协议 HTTP看起来是最“基础”的一层但它同时是面试里被问得最碎、线上排障时被查得最多的一层。报文怎么组织、请求方法怎么选、状态码怎么定、常见字段怎么配合这四件事串起来就是一条完整的请求生命周期。我写这篇的出发点很直接把面试里那些零散的问题还原成一张能用的图。刚入行的同学可以把它当成一份带注解的速查手册照着敲一遍命令就能看到效果有几年经验的同学可以重点看报文边界、幂等性、缓存协商和状态码语义这几块这些地方最容易在设计和排障时踩坑。文中涉及的操作都基于公开工具和标准协议参数和计算过程我会尽量写清楚方便你直接复现。1. 先把 HTTP 这张地图铺开整体框架与考点拆解1.1 面试官问 HTTP真正想听的是什么很多人把 HTTP 当成背诵题背完九个请求方法、背完五大类状态码就结束了。但我在实际面试和带人的过程中发现真正拉开差距的不是“能不能背出来”而是“能不能在具体场景里做选择”。比如同样是一个“修改用户资料”的接口用PUT还是PATCH用POST加动作名还是用PUT加资源路径背后涉及的是幂等性、缓存行为和代理重试策略不是命名喜好问题。面试官问“GET 和 POST 有什么区别”如果只回答“参数位置不同、长度限制不同”基本就停在表层了。更有价值的回答会往下走一层GET 是安全方法语义上是“读取”可以被缓存、可以被浏览器预取、可以出现在历史记录里POST 语义上是“提交”默认不缓存、不可幂等重试。至于“长度限制”那根本不是协议规定的而是早期浏览器和服务器实现的约束HTTP/1.1 报文本身对 URL 长度没有硬性上限限制来自具体实现。再比如问“301 和 302 的区别”能说出“永久和临时”只是及格线。往下一层是301 会被浏览器和中间缓存长期记住如果你把 301 用在一个还会变的跳转上用户可能几个月都回不到新地址302 在历史上被很多客户端实现成了“把 POST 改成 GET 再跳”所以后来才补了 307 和 308 来严格保持方法。这些才是线上会真正咬人的地方。所以这一章我想先建立一个共识HTTP 的知识点不是并列的而是有层次的。最底层是报文格式和连接模型中间层是方法、状态码、头部字段这些语义约定最上层才是缓存、跨域、内容协商这些组合行为。面试题往往从最上层提问但答案要落回下面两层。你如果只记最上层遇到追问就会露馅反过来底层清楚了上层是可以现场推出来的。1.2 一条可复用的知识骨架从 URL 到响应落地我习惯用一条主线把零散知识串起来URL 解析 → 建立连接 → 构造请求报文 → 服务端处理 → 返回响应报文 → 客户端按状态码和头部决定下一步。这条线上每一个箭头都对应一类面试题。URL 解析阶段考的是 scheme、host、port、path、query、fragment 的划分以及 URI 和 URL 的关系。URI 是标识符URL 是定位符URL 是 URI 的子集这个区分不复杂但常被问。这里还有一个容易忽略的点fragment#后面的部分不会发给服务端它只在客户端生效所以任何指望用 fragment 传参给后端的做法都是错的。建立连接阶段考的是 TCP 三次握手、TLS 握手、HTTP/1.1 的持久连接、HTTP/2 的多路复用。这一层的关键词是“连接复用”因为它直接决定了性能优化的方向。HTTP/1.1 默认开启持久连接一个 TCP 连接上可以串行发多个请求HTTP/2 在一条连接上并行跑多个流HTTP/3 干脆把传输层换成了基于 UDP 的 QUIC。版本演进的主线其实就是不断解决“前面堵住了”的问题。构造报文阶段考的是起始行、头部字段、空行、消息体这四段的顺序和边界以及Content-Length和Transfer-Encoding的关系。这一段是纯格式问题最容易被忽略但排查“请求体被截断”“服务端读不到 body”这类问题时往往就卡在这里。服务端处理阶段考的是方法语义、幂等性、安全性以及服务端如何根据Accept、Accept-Encoding做内容协商。返回响应阶段考的是状态码语义、缓存字段、Location、Set-Cookie、CORS 相关头部。这两段是组合行为最多的地方也是面试追问最密集的地方。把这条主线画在纸上你会发现所谓“八股”其实是十几个彼此咬合的模块。面试时遇到没准备过的问题顺着主线往回推通常能推到一个你会的点上。1.3 版本演进不是背年份而是理解设计取舍HTTP/0.9 只有一个GET响应只有 HTML 正文没有头部HTTP/1.0 补上了头部、状态码、多种方法但默认每个请求一个连接用完就关HTTP/1.1 把持久连接变成默认加了Host头、分块传输、更多的缓存控制字段HTTP/2 改成二进制分帧支持多路复用和头部压缩HTTP/3 基于 QUIC把可靠传输做进了用户态。这条线里最值得理解的是“为什么要有Host”。HTTP/1.0 时代一个 IP 地址通常只对应一个站点所以请求里不需要说明“我要访问哪个域名”。后来一台服务器上要放多个站点共享同一个 IP就必须在请求头里带上域名服务端才能路由到正确的虚拟主机。Host因此成了 HTTP/1.1 里唯一一个强制要求的请求头缺了它服务器可以直接返回 400。另一个值得理解的是“为什么 HTTP/2 要二进制分帧”。HTTP/1.1 的报文是纯文本解析简单但天然不适合在同一条连接上交错发送多个请求的片段因为文本流没有办法标记“这一段属于哪个请求”。二进制分帧把报文切成带流 ID 的帧接收方就能按流 ID 重新组装多路复用才成立。代价是可读性下降抓包不能再靠肉眼看文本必须用支持 HTTP/2 解析的工具。至于 HTTP/3 换成 UDP核心动机是解决 TCP 层的队头阻塞TCP 保证字节流有序一旦某个包丢了后面所有数据都得等它重传哪怕后面的数据属于另一个请求。QUIC 在用户态实现流控和重传不同流之间互不影响同时把 TLS 握手合并进去减少往返次数。理解了这个动机你就不会把“HTTP/3 用 UDP”记成一句孤立的口号。2. 报文结构与传输细节把文本协议拆到字节级2.1 起始行、头部、空行与消息体四段的边界在哪一个 HTTP 报文无论请求还是响应结构都可以概括成四段起始行、头部字段、一个空行、可选的消息体。请求的起始行是方法 请求目标 协议版本响应的起始行是协议版本 状态码 原因短语。空行是头部和消息体的分界线这一行不能省哪怕没有消息体也要有。很多手写报文调试失败就是因为少敲了一个回车。POST /api/users HTTP/1.1 Host: example.internal Content-Type: application/json Content-Length: 27 {name:A,age:18}上面这个例子里Content-Length: 27是怎么来的把消息体{name:A,age:18}按字节数一遍{、、n、a、m、e、、:、、A、、,、、a、g、e、、:、1、8、}一共 21 个 ASCII 字符每个 1 字节21 字节。但我上面写的是 27如果真按 27 发服务端会认为后面还有 6 个字节没到于是继续等直到超时。这就是Content-Length算错的典型表现请求看起来发出去了服务端却一直挂着。中文场景更容易错。UTF-8 下一个常用汉字通常占 3 字节{name:张三}里“张三”两个字就是 6 字节整个消息体是 21 字节。如果你按字符数填Content-Length就会少 4 个字节服务端读到一半就断了报错通常是“请求体不完整”或 JSON 解析失败。这类问题在日志里看起来像编码问题实际是长度声明问题。我个人的习惯是只要手写报文就先不加Content-Length改用Transfer-Encoding: chunked让客户端自己分块如果用工具发请求就让工具自动计算。手工算长度只在排查问题时做而且要拿字节数计算器核对一遍。2.2 Content-Length 与 Transfer-Encoding两个只能留一个Content-Length和Transfer-Encoding: chunked是互斥的同时出现属于协议层面的危险信号。原因很直白前者说“消息体固定这么多字节”后者说“消息体是一块一块来的最后用零长度块结束”两套规则打架接收方无法确定边界。分块传输的格式是每块先写十六进制的块长度回车换行再写块内容回车换行最后写一个0加空行表示结束后面还可以跟可选的尾部字段。它的价值在于发送方不需要事先知道消息体总长度适合流式生成内容、压缩后长度未知、或者服务端边算边发的场景。HTTP/1.1 200 OK Content-Type: text/plain Transfer-Encoding: chunked 7\r\n Mozilla\r\n 9\r\n Developer\r\n 7\r\n Network\r\n 0\r\n \r\n这里有个细节值得注意块长度是十六进制7就是 7 字节9是 9 字节A才是 10 字节。我见过有人在调试时把10当成十进制写进去结果接收方按十六进制解析成 16直接错位。这种错误在纯文本抓包里不明显因为后面跟的正好是字母肉眼看不出问题但程序解析就会炸。另一个相关字段是TE它用在请求头里告诉服务端“我能接受哪种传输编码”常见值是trailers。它和Transfer-Encoding不是一回事前者是能力声明后者是实际使用的编码方式面试里偶尔会拿来混淆。注意如果响应同时出现Content-Length和Transfer-Encoding不同代理和框架的处理策略可能不一样有的以Transfer-Encoding为准有的直接拒绝。作为服务端开发者最稳妥的做法是保证二者不同时出现并且在校验层加一条规则把它拦掉。2.3 持久连接、管线化与队头阻塞HTTP/1.1 默认开启持久连接也就是一个 TCP 连接上可以连续发多个请求不需要每次重新握手。控制它的头部是Connectionkeep-alive表示希望复用close表示用完就关。服务端通常还会带一个Keep-Alive: timeout5, max1000这样的提示意思是空闲 5 秒后关闭、这个连接最多处理 1000 个请求。这两个值是提示不是强制客户端可以不听。持久连接解决了“每次请求都要三次握手”的开销但没解决“同一连接上请求必须按顺序处理”的问题。HTTP/1.1 的管线化允许客户端连续发多个请求而不等响应但服务端必须按顺序返回前一个请求慢后面的响应就只能排队。这就是 HTTP/1.1 层面的队头阻塞。浏览器对这个问题的实际解法是“开多个连接”常见做法是对同一个域名开 6 个左右的并发连接。代价是连接数上去了服务端要维护更多状态而且每个连接都要单独握手。前端优化里常说的“域名分片”本质就是绕过单域名连接数限制把资源分散到多个子域名上。但这招在 HTTP/2 下反而有害因为 HTTP/2 一条连接就能跑多路复用域名分片会破坏连接复用增加握手开销。HTTP/2 的多路复用把“请求”拆成了“流”每个流有独立 ID帧可以交错发送接收方按流 ID 重组。这样一条连接上可以并行处理多个请求队头阻塞从应用层被消除了。但 TCP 层的队头阻塞还在一个包丢了整条连接上的所有流都得等重传。HTTP/3 换用 QUIC 就是为了把这个也解决掉。这里有一组数字可以帮你建立直觉假设一次 TCP 握手加 TLS 握手需要 3 个往返每个往返 40 毫秒那么新建连接的成本大约是 120 毫秒。如果一个页面有 60 个资源用 HTTP/1.0 的做法每个资源新建连接光握手就是 7.2 秒用 HTTP/1.1 持久连接加 6 个并发握手成本摊到 10 条连接上大约 1.2 秒用 HTTP/2 一条连接多路复用握手成本只有 120 毫秒。这个测算当然是理想化的没算带宽和解析时间但它能解释为什么“减少连接数”在 HTTP/2 时代从优化手段变成了默认前提。3. 请求方法幂等性、安全性与接口设计3.1 九个方法的定位与使用场景标准里定义的请求方法有九个GET、HEAD、POST、PUT、DELETE、PATCH、OPTIONS、TRACE、CONNECT。日常业务里高频使用的是前六个后三个更多出现在框架和代理层。GET用于获取资源HEAD和它几乎一样区别是响应不返回消息体只返回头部。HEAD的实用价值在于“先探一下”想知道一个文件多大、最后修改时间是什么、是否还存在用HEAD比GET省流量。我见过用GET去探测大文件是否存在的写法文件几百兆探测一次就把带宽吃掉了换成HEAD只回几十字节的头部。POST用于提交数据语义上不幂等重复提交可能产生多条记录。PUT用于“把资源放到指定位置”语义上是幂等的同一个请求发一次和发十次最终状态一样。DELETE删除资源也是幂等的删一次和删十次结果都是“不存在”。PATCH用于局部修改标准里没有强制要求它幂等实际实现里也常常不幂等比如“把计数加一”这种补丁。OPTIONS有两个用途一是查询资源支持哪些方法响应里会带Allow头二是 CORS 预检请求浏览器在发跨域的非简单请求之前会先用OPTIONS问一句“我能不能发”。TRACE用于回显请求方便调试但它有信息泄露风险绝大多数生产环境会直接禁用。CONNECT用于代理场景建立隧道日常业务开发基本接触不到了解它存在即可。3.2 幂等与安全为什么 PUT 和 DELETE 可以重试“安全”和“幂等”是两个不同的概念面试里经常被混着问。安全方法指的是“不改变服务端资源状态”GET、HEAD、OPTIONS、TRACE属于这一类。幂等方法指的是“执行一次和执行多次服务端最终状态相同”GET、HEAD、PUT、DELETE、OPTIONS、TRACE属于这一类。注意POST既不安全也不幂等PATCH通常既不安全也不幂等。这个区分为什么重要因为它直接决定了“能不能自动重试”。网络抖动导致请求超时客户端并不知道服务端到底处理了没有。如果方法是幂等的客户端可以放心重发最坏结果就是多做一次无副作用的操作如果方法不幂等重发就可能产生重复订单、重复扣款、重复消息。POST /orders → 创建一个订单重试可能创建两个 PUT /orders/123 → 把 123 号订单设置成指定内容重试结果一样 DELETE /orders/123 → 删除 123 号订单重试结果一样第二次返回 404 或 204 都合理 PATCH /orders/123 → {op:increment,field:count}重试会多加一次我实际遇到过的一个坑是某个内部服务把“更新用户余额”做成了PUT但实现里写的是“在当前余额上加减”而不是“设置成目标余额”。语义上是PUT行为上却是非幂等的。结果网关在超时后自动重试了一次余额被加了两次。这类问题的根因不是 HTTP 用错了而是“方法语义”和“实现行为”不一致。修复方式有两种要么把接口改成真正的幂等实现传目标值而不是增量要么承认它不幂等改用POST并且关掉网关的自动重试。所以我在做接口设计时有个习惯先问一句“这个操作重发一次会怎样”。如果答案是“会出问题”那就要么设计成幂等要么引入幂等键比如请求头里带一个唯一的Idempotency-Key服务端去重。幂等键这个做法不是 HTTP 标准的一部分但它是业界很常见的补充手段尤其在支付、下单这类场景里。3.3 GET 与 POST 的真实差异别再说“参数位置不同”“GET 参数在 URL 里POST 参数在 body 里”这句话只描述了现象没解释原因。真正的差异在语义GET是“我要读取一个资源”它的请求目标本身就标识了资源所以参数放在 URL 里是自然的POST是“我要提交一份数据给服务端处理”数据是内容所以放在 body 里。由此衍生出一串实际差异。GET请求可以被浏览器缓存、可以被加入书签、可以出现在历史记录里、可以被预取这些都是“读取”语义带来的好处。POST默认不缓存刷新页面时浏览器会提示“是否重新提交表单”因为它不知道重复提交是否安全。关于“长度限制”需要澄清一下HTTP 协议本身没有规定 URL 最大长度限制来自浏览器、服务器、代理的具体实现。常见浏览器对 URL 的长度限制在几万字符量级服务器端常见配置是 8KB 左右。所以“GET 只能传 2KB”是一个流传很广但不准确的说法。真正该关心的不是长度而是“这些参数是否适合出现在 URL 里”URL 会被记录在访问日志、浏览器历史、代理日志、Referer 头里所以密码、令牌、身份证号这类敏感信息不应该放在 URL 里。还有一个常被忽略的点GET请求带 body 在协议上并不禁止只是语义上没有定义很多服务器和代理会直接忽略或拒绝。所以不要依赖GET带 body 来传数据兼容性风险很大。POST的Content-Type也值得说一句。表单提交用application/x-www-form-urlencoded文件上传用multipart/form-data接口通信用application/json。这三种的报文格式完全不同服务端解析方式也不同。我见过前端发了 JSON后端按表单解析结果拿到一堆空值查了半天以为是字段名写错其实是Content-Type没对上。4. 状态码语义、边界与线上排障经验4.1 五大类状态码的速记逻辑状态码是三位数字第一位决定大类1xx表示信息性请求已收到继续处理2xx表示成功3xx表示重定向需要进一步动作4xx表示客户端错误5xx表示服务端错误。这个分类不是随便分的它决定了客户端和中间件的行为。比如缓存代理看到2xx才考虑缓存看到5xx可能会重试或熔断看到4xx一般直接把错误透传给调用方。1xx在实际业务里很少见但有两个值得知道100 Continue和101 Switching Protocols。前者配合请求头Expect: 100-continue使用客户端先问一句“我要发一个大 body你收不收”服务端回100再发避免白传后者用于协议升级WebSocket 握手成功时返回的就是101。2xx里最常用的是200 OK表示请求成功并且响应体里有内容。201 Created表示资源创建成功通常配合Location头指向新资源。204 No Content表示成功但没有响应体适合DELETE成功或者PUT更新成功且不需要返回内容的场景。206 Partial Content用于范围请求视频拖动进度条、大文件断点续传都靠它。3xx是重定向家族坑最多。301 Moved Permanently是永久重定向会被缓存302 Found是临时重定向历史上客户端会把POST改成GET303 See Other明确要求用GET去访问新地址307 Temporary Redirect要求保持原方法不变308 Permanent Redirect是307的永久版本。304 Not Modified比较特殊它不是重定向而是“缓存还有效别传内容了”必须配合条件请求头使用。4xx表示客户端侧的问题。400 Bad Request是笼统的“请求格式有问题”401 Unauthorized其实是“未认证”需要提供凭证403 Forbidden是“已认证但无权限”404 Not Found是资源不存在405 Method Not Allowed是方法不支持响应里应该带Allow头说明支持哪些方法409 Conflict用于状态冲突比如重复创建429 Too Many Requests用于限流通常带Retry-After。5xx表示服务端侧的问题。500 Internal Server Error是兜底的“服务端出错了”501 Not Implemented是“这个功能没实现”502 Bad Gateway是网关从上游收到了无效响应503 Service Unavailable是服务暂时不可用通常带Retry-After504 Gateway Timeout是网关等上游超时。502和504是排查链路问题时最常见的两个码502往往是上游进程崩了或者返回了非法内容504往往是上游处理太慢。4.2 容易被追问的十几个状态码301和302的区别前面提过这里补一个实际影响如果给一个 API 接口配了301客户端可能会长期缓存这个跳转等你后来想改回去发现一部分客户端还在往老地址跳。所以 API 场景下临时跳转优先用302或307确定永久变更再用301或308。401和403也经常被混。401的核心是“你还没证明你是谁”响应里应该带WWW-Authenticate头告诉客户端怎么认证403的核心是“我知道你是谁但你不能干这个”。如果用户没登录返回401更准确如果用户登录了但角色不够返回403更准确。有些系统统一返回403来避免泄露“资源是否存在”这是安全权衡不算错但要在文档里说清楚。400和422的边界在于“格式错误”和“语义错误”。请求体不是合法 JSON属于400JSON 合法但字段缺失、类型不对、业务校验不通过用422 Unprocessable Entity更精确。当然很多团队统一用400也没问题关键是团队内部要有一致的约定别同一个项目里有的地方400、有的地方422。405必须带Allow头这是标准要求。我见过接口返回405但不带Allow调用方完全不知道该怎么改。加上一行Allow: GET, POST成本极低收益很大。409适合“资源状态冲突”比如用PUT创建一个已存在的资源或者乐观锁版本号对不上。412 Precondition Failed适合条件请求失败比如If-Match的 ETag 不匹配。这两个码在并发控制场景里很有用但实际使用率不高面试里能说出来是加分项。429在限流场景里应该优先于503。429明确表示“你请求太多了”客户端看到它应该退避503表示“服务整体不可用”可能是对方在发版或者过载。两者带Retry-After都能帮助客户端做正确的退避而不是立刻重试把对方打得更惨。4.3 状态码选错带来的实际后果状态码不只是给调用方看的提示它会被整条链路上的组件消费。缓存代理会根据状态码决定是否缓存负载均衡会根据状态码决定是否把节点摘掉监控系统会根据状态码统计错误率并触发告警客户端 SDK 会根据状态码决定重试策略。选错一个码可能在某个环节引发连锁反应。举个常见的例子某个接口在业务校验失败时返回了500。开发者的想法是“反正都是失败500 也能表示”但监控系统会把500计入服务端错误率错误率一高就触发告警值班同学半夜被叫起来排查最后发现是用户输错了参数。这类“假警报”消耗的是团队对监控的信任时间长了大家对告警麻木真出问题时反而没人当回事。另一个例子是把限流返回成403。调用方看到403会认为“我没有权限重试也没用”于是直接失败如果返回429并带上Retry-After: 2调用方就知道“等两秒再试”成功率会高很多。这就是语义准确带来的实际收益。还有一个反向的坑有的团队为了“不让前端难做”所有错误都返回200在响应体里放{code: 500, msg: ...}。这种做法在简单页面里能跑但一旦接入网关、监控、重试组件全都失效因为链路上的每个组件都认为“请求成功了”。如果你所在的团队还在这么做至少要在网关层做一层转换把业务错误码映射回 HTTP 状态码否则可观测性基本为零。5. 常见头部字段缓存、协商、连接与安全5.1 内容协商与实体头部让同一份资源有多种形态内容协商解决的是“同一个 URL不同客户端想要不同格式”的问题。请求方用Accept声明想要的媒体类型用Accept-Language声明语言用Accept-Encoding声明能接受的压缩算法服务端根据这些头选择一种表示并在响应里用Content-Type、Content-Language、Content-Encoding告诉客户端实际返回的是什么。Content-Type和Accept经常被搞混。Accept是请求头意思是“我能接受什么”Content-Type在请求里表示“我发的是什么”在响应里表示“我返回的是什么”。一个是能力声明一个是事实描述。服务端如果发现请求的Content-Type不支持应该返回415 Unsupported Media Type如果发现Accept里没有自己能提供的类型可以返回406 Not Acceptable。这两个码在实际项目里用得不多但用对了能省很多沟通成本。Content-Encoding是压缩相关的。常见值是gzip、br、deflate。浏览器发请求时带Accept-Encoding: gzip, br服务端可以选择其中一种压缩响应体并在响应头里声明。这里有个容易忽略的点压缩改变的是消息体的字节流Content-Length要填压缩后的长度不是原始长度。如果服务端先算了原始长度、再压缩却忘了更新Content-Length客户端就会读到截断的内容。Vary头是内容协商里最容易被漏掉的一个。它的作用是告诉缓存“这个响应的内容会随某个请求头变化缓存时要把这个头也算进 key 里”。最典型的场景是服务端根据Accept-Encoding返回 gzip 或未压缩的内容如果响应里没有Vary: Accept-Encoding中间缓存可能把 gzip 版本缓存下来然后返回给一个不支持 gzip 的客户端客户端解析直接失败。加上这一行缓存就会区分两种版本问题消失。Range和Content-Range用于范围请求。客户端发Range: bytes0-1048575表示想要前 1MB服务端返回206并在Content-Range: bytes 0-1048575/10485760里说明“这是 10MB 文件的前 1MB”。视频播放器拖动进度条、下载工具多线程下载、断点续传都依赖这套机制。如果服务端不支持范围请求可以返回200和完整内容客户端会退化成普通下载这也是标准允许的行为。5.2 缓存字段强缓存与协商缓存怎么配合HTTP 缓存分两层强缓存和协商缓存。强缓存命中时浏览器直接用本地副本一个字节都不发协商缓存命中时浏览器会发一个条件请求服务端返回304告诉它“没变继续用本地的”这样只传头部不传正文。强缓存由Cache-Control和Expires控制。Expires是一个绝对时间比如Expires: Wed, 21 Oct 2026 07:28:00 GMT它的致命弱点是依赖客户端本地时间用户把系统时间改一下缓存就乱了。Cache-Control: max-age3600是相对时间从响应生成那一刻算起 3600 秒内有效不受本地时间影响。所以现代服务端优先用max-ageExpires只作为兼容老客户端的兜底。Cache-Control的常用值需要分清楚max-age是客户端缓存时长s-maxage是共享缓存比如 CDN、代理的时长优先级高于max-ageno-cache不是“不缓存”而是“可以缓存但每次使用前必须向服务端确认”也就是走协商缓存no-store才是真正的“不许缓存”任何环节都不能存public表示响应可以被任何缓存保存private表示只能被客户端缓存不能被共享缓存保存must-revalidate表示过期后必须重新验证不能用过期副本凑合。这里最容易错的是no-cache。很多人以为它是“禁用缓存”结果配了之后发现请求还是走了缓存只是每次都带条件请求。真正想禁用缓存要用no-store。这两个词只差一个字母行为差别很大面试里追问的概率很高。协商缓存靠两组头配合ETag和If-None-MatchLast-Modified和If-Modified-Since。ETag是服务端给资源算的一个标识可以是内容的哈希也可以是版本号客户端下次请求时把ETag放进If-None-Match服务端比对一致就返回304。Last-Modified是最后修改时间客户端下次请求时放进If-Modified-Since服务端比对时间就返回304。两者相比ETag更精确因为它基于内容而不是时间。如果一个文件在一秒内被修改了两次Last-Modified只精确到秒就可能误判“没变”ETag基于内容哈希就不会有这个问题。另外Last-Modified只能表示时间无法表达“内容没变但修改时间变了”的情况比如文件被重新部署但内容一样。所以优先级上ETag高于Last-Modified两者同时存在时服务端应该优先校验If-None-Match。这组机制省多少流量可以算一笔账一个接口返回 200KB 的 JSON如果每次都全量返回1000 次请求就是 200MB如果启用协商缓存命中时返回304大约只有 200 字节的响应头1000 次里 900 次命中流量大约是 200KB × 100 200B × 900约 20MB省了 90%。如果是静态资源、图片这类大文件节省比例还能更高。5.3 连接管理、Cookie 与跨域相关字段Connection用来控制连接是复用还是关闭keep-alive和close两个值前面提过。HTTP/1.1 默认复用所以通常不需要显式写keep-alive写close表示这个请求处理完就断开适合需要快速释放连接或者明确不想复用的场景。Host是 HTTP/1.1 必填的请求头用来指定目标域名和端口。它的作用是支持虚拟主机一台服务器上跑多个站点靠Host区分。如果请求里缺Host服务端返回400是标准行为。反向代理在转发时通常会重写Host或者在转发头里保留原始Host排查“为什么后端拿到的域名不对”时先看这两个头。Cookie和Set-Cookie是无状态协议上的状态保持机制。Set-Cookie由服务端下发可以带Domain、Path、Expires或Max-Age、Secure、HttpOnly、SameSite这些属性。HttpOnly让 Cookie 不能被 JavaScript 读取降低 XSS 风险Secure让 Cookie 只在加密连接上发送SameSite控制跨站请求是否携带 CookieStrict最严Lax是常见默认值None必须配合Secure。Origin和Referer都表示“请求从哪来”但用途不同。Referer是页面的完整或部分 URL可能包含敏感信息Origin只包含协议、域名和端口不带路径更适合用于安全校验。CORS 预检请求里浏览器会带Origin服务端根据它决定是否返回Access-Control-Allow-Origin。CORS 是一整套头Access-Control-Allow-Origin声明允许的来源Access-Control-Allow-Methods声明允许的方法Access-Control-Allow-Headers声明允许的自定义头Access-Control-Max-Age声明预检结果能缓存多久Access-Control-Allow-Credentials声明是否允许携带凭证。一个常见坑是当Allow-Credentials为true时Allow-Origin不能是*必须是具体的来源否则浏览器会拒绝。另一个坑是预检请求本身不带 Cookie所以服务端不能依赖 Cookie 来做预检的鉴权。6. 动手验证用最小成本把理论跑一遍6.1 用 curl 看请求与响应全貌理论看再多不如自己发一个请求看报文。curl是最轻量的工具加上-v就能看到完整的请求头和响应头。curl -v -X POST http://example.internal/api/users \ -H Content-Type: application/json \ -H Accept: application/json \ -d {name:A,age:18}输出里以开头的行是客户端发出的请求以开头的行是服务端返回的响应*开头的是连接过程信息。你可以清楚地看到请求行、Host、Content-Type、Content-Length以及响应的状态行和各个头部。注意curl默认的Content-Type是application/x-www-form-urlencoded如果不显式指定服务端可能按表单解析拿到的东西跟你想的不一样。只看响应头不想要正文可以用-I它发的是HEAD请求。想看完整的请求和响应但不想被连接信息干扰可以用-s -o /dev/null -D -把响应头单独打印出来。想强制使用 HTTP/1.1 或 HTTP/2可以用--http1.1或--http2对比两种协议下头部的差异能直观看到 HTTP/2 的头部压缩效果。如果你想看更底层的字节流可以用telnet或nc手工敲报文。这种方式比较原始但对理解“空行是分界线”特别有帮助。手工敲的时候注意每行结尾要按回车头部结束后要多按一次回车否则服务端会一直等你把头部发完。6.2 用浏览器开发者工具看缓存命中打开开发者工具的网络面板勾选“禁用缓存”之外的选项刷新页面观察每个请求的“大小”列。如果显示(来自内存缓存)或(来自磁盘缓存)说明强缓存命中请求根本没发出去。如果状态码是304说明走的是协商缓存请求发出去了但响应体是空的。在响应头里找Cache-Control、ETag、Last-Modified在请求头里找If-None-Match、If-Modified-Since你就能看到两组头是怎么配对的。可以做个实验先正常访问一次然后在地址栏回车再访问一次观察第二次请求的头部多了什么、响应码变成了什么。再强制刷新一次一般是CtrlF5或CmdShiftR观察请求头里是不是多了Cache-Control: no-cache告诉服务端“别用缓存给我最新的”。这个实验的价值在于把“强缓存”“协商缓存”“强制刷新”这三个概念对应到具体的请求上。很多人背得出定义但真看到304时不确定“这算命中还是没命中”。答案是从流量角度算命中因为没传正文从请求次数角度算发出了请求只是响应很短。6.3 构造一个带 Range 的分段下载准备一个大于 1MB 的文件放到能访问的路径下然后发范围请求curl -v -r 0-1048575 http://example.internal/static/demo.bin -o part1.bin-r 0-1048575等价于Range: bytes0-1048575表示取前 1048576 字节。如果服务端支持会返回206 Partial Content并带上Content-Range: bytes 0-1048575/文件总大小。你可以再用-r 1048576-取剩下的部分然后把两个文件拼接起来校验和跟原文件一致就说明范围请求生效了。这个实验能顺带验证几件事服务端是否支持Range、是否正确处理了Accept-Ranges、Content-Length是不是分片的长度而不是总长度。如果服务端返回200而不是206说明它不支持范围请求客户端会退化成整文件下载。7. 常见问题与排查实录7.1 状态码与字段相关的典型故障第一个高频问题接口返回 200 但数据不对。先看Content-Type和实际返回内容是否匹配。我曾经遇到过服务端返回了 HTML 错误页但状态码是200前端按 JSON 解析失败抛了一个“解析异常”真正的错误被掩盖了。这种情况要查服务端是不是有统一的异常处理把错误吞掉了。第二个问题POST 请求体在服务端读不到。按顺序查三件事Content-Type是否正确、Content-Length是否和实际字节数一致、是否有代理改写了请求。如果用了Transfer-Encoding: chunked还要确认服务端框架是否支持分块读取。有些老框架只认Content-Length遇到分块请求直接读空。第三个问题缓存导致更新不生效。先看响应头里的Cache-Control和ETag确认强缓存时长是不是太长。如果配了max-age86400用户一天内看到的都是旧版本除非强制刷新。解决办法不是让用户清缓存而是给静态资源加内容哈希文件名或者把 HTML 的缓存策略设为no-cache让每次访问都走协商缓存。第四个问题跨域请求失败但接口本身正常。打开网络面板看预检请求确认OPTIONS是否返回了正确的Access-Control-Allow-*头。常见错误是只配了Allow-Origin没配Allow-Headers导致自定义头被拦或者Allow-Credentials为true时Allow-Origin写了*浏览器直接拒绝。第五个问题网关返回 502 或 504。502优先查上游进程是否存活、端口是否监听、返回内容是否符合 HTTP 格式504优先查上游处理耗时、超时配置是否合理、是否有慢查询或锁等待。这两个码的排查方向完全不同别混在一起查。7.2 一张速查表现象优先怀疑的字段或状态码排查动作请求体被截断Content-Length按字节数核对注意中文占 3 字节服务端一直等不到请求结束Transfer-Encoding检查分块结束标记是否为0加空行更新后页面还是旧内容Cache-Control、ETag看强缓存时长确认是否走协商缓存304 响应但内容不对ETag生成逻辑确认 ETag 是否基于内容而不是随机值跨域预检失败Access-Control-Allow-*检查方法、头部、凭证三项是否匹配接口偶发重复数据方法幂等性确认POST是否被自动重试考虑幂等键401 和 403 混用WWW-Authenticate未认证用 401无权限用 403限流后客户端疯狂重试429、Retry-After补上退避提示避免雪崩下载工具无法断点续传Range、Accept-Ranges确认服务端返回 206 和Content-Range压缩后内容解析失败Content-Encoding确认客户端解压能力与响应声明一致这张表不是穷举但它覆盖了我实际遇到的大部分问题。用的时候注意一点先确认“问题出在哪一层”再查对应的字段。如果是浏览器行为优先看请求头如果是服务端行为优先看响应头如果是中间代理行为两边的头都要看因为代理可能会改写字段。7.3 面试追问的应对思路面试里问 HTTP通常会沿着“是什么 → 为什么 → 怎么做 → 出问题怎么查”这条线往下挖。准备的时候可以按这个层次组织答案而不是背零散的知识点。比如问“说说缓存”第一层答强缓存和协商缓存两类第二层答Cache-Control和ETag的设计意图为什么要有相对时间和内容标识第三层答具体配置静态资源长缓存加哈希、HTML 短缓存或协商缓存第四层答排障怎么判断命中、怎么让更新立即生效。四层都答到基本就是满分答案。再比如问“POST 和 PUT 的区别”不要停在“一个创建、一个更新”要说到幂等性、URL 语义、重试策略、缓存行为。如果能补一句“PUT 的 URL 应该标识资源本身POST 的 URL 标识处理入口”面试官会觉得你真的用过而不是背过。还有一个常被问的点是“HTTP 是无状态的那登录状态怎么保持”。标准答案是 Cookie 加 Session或者 Token。但更有价值的是补一句“无状态是协议层面的设计目的是让每个请求自包含、便于水平扩展状态保持是靠应用层补的代价是要考虑存储、过期、跨域和安全”。这样回答就把协议设计和工程实践连起来了。最后一个建议面试时遇到不确定的细节不要硬编。可以说“这个字段的具体行为我记不太准但按语义推测应该是……我回去查一下标准”。面试官更在意你的推理过程和学习态度而不是你把 RFC 背得多熟。我个人在实际项目里的体会是HTTP 这一层真正值钱的不是记住多少状态码而是养成“先看报文”的习惯。遇到接口问题先抓一个请求和响应把起始行、头部、空行、正文逐段看一遍大部分问题在五分钟内就能定位。这个习惯一旦建立很多所谓的“玄学问题”都会变得有迹可循。