☰
HTTP报文、请求方法与状态码:面试考点与线上排坑指南
2026/10/26 11:59:44 网站建设 项目流程

去年年底帮一个团队排查过一个挺典型的问题:同一个接口在测试环境跑得好好的,上了生产、经过一层网关之后,返回码从 200 变成了 405,前端同学第一反应是后端偷偷改了请求方法,后端同学翻遍代码说没有。最后抓包一看,是网关的转发规则把 PUT 当成需要额外授权的方法拦掉了,请求根本没走到应用层。这类问题往深了说都不复杂,难的是一堆人围在一起,没人能把 HTTP 报文、请求方法、状态码、字段这几件事串起来讲清楚,于是只能靠猜。

这也是为什么 HTTP 相关的问题在面试里几乎必问:它看起来是最基础的知识,实际上能把一个人对网络分层、无状态设计、缓存策略、网关行为的理解全部照出来。这篇文章想做的事情很直接——把超文本传输协议 HTTP 的基本概念、报文结构、常用请求方法、状态码和常见字段这五块内容,按“面试会怎么问、实际会怎么坑”的顺序重新捋一遍。刚入行的同学可以把它当成一张自测清单,工作两三年、平时只写业务代码的同学,也可以拿它补一补那些被框架封装掉的细节。

我不打算把规范文档逐条翻译一遍,那没意义。下面写到的每一个点,都会尽量回答两个问题:它为什么这么设计,以及它在真实项目里会以什么形式咬你一口。

1. 把 HTTP 看成一次对话:概念题的答题框架

1.1 应用层的请求-响应模型到底在说什么

HTTP 是一个应用层协议,工作在 TCP 之上(HTTP/3 换成了基于 UDP 的 QUIC),它的核心模型非常简单:客户端主动发起一个请求,服务端返回一个响应,一问一答,结束。名字里的“超文本”是历史遗留叫法,早期主要用来传 HTML 文档,现在 JSON、图片、视频流、protobuf 二进制,什么都能装进去,本质上它就是一个“约定好格式的数据搬运工”。

面试里问“HTTP 是什么”,很多人会答“超文本传输协议”,然后就没了。这个答法等于没答,因为面试官想听的是:它属于哪一层、依赖什么传输层协议、通信模型是什么、有什么典型特征。比较完整的答法是——应用层协议,默认端口 80(HTTPS 是 443),基于请求-响应模型,客户端主动发起,服务端被动响应,报文是文本格式(1.x 版本),协议本身无状态,通过首部字段具备良好的可扩展性。

再往下追问一句“HTTP 和 TCP 什么关系”,能答出“HTTP 定义了消息长什么样、怎么交互,TCP 负责把字节可靠地送到对面,HTTP 不关心底层怎么传”就基本过关了。如果面试官继续问“那 HTTP/3 为什么不走 TCP”,这就是在考你对队头阻塞的理解,后面版本演进那节会详细讲。

1.2 无状态这件事,代价和收益都要讲清楚

“HTTP 是无状态协议”这句话几乎人人会背,但很少有人能说清楚无状态到底意味着什么。它的意思是:协议层面不要求服务端记住上一次请求的任何信息,每个请求都是独立的,服务端处理完就可以把这次会话的所有上下文丢掉。好处很直接——服务端不需要为每个用户维持上下文,任何一个节点都能处理任意一个请求,水平扩容特别容易,某个节点挂了换个节点接着处理就行。

代价同样明显:登录状态怎么保持?购物车怎么记住?答案是把状态挪到协议之外——Cookie 加 Session、Token、或者干脆每次请求都带上凭证。这里有个常见的追问链路:Cookie 和 Session 有什么区别?可以这么理解,Cookie 是存放在客户端的一小段数据,Session 是存放在服务端的一份状态,二者通过一个 Session ID 关联起来;而 Token 把状态编码进凭证本身,服务端只需要验签不需要存储,所以在多节点部署时更省事。

还有一个容易答错的点:无状态不等于不能用长连接。HTTP/1.1 默认开启的 keep-alive 是传输层的连接复用,复用同一条 TCP 连接发送多个请求,这跟协议语义上的无状态是两回事,两者并不矛盾。面试时把这句话说出来,通常能加分。

1.3 一次请求在协议层大致经历了什么

从输入网址到看到页面,粗略拆一下:域名解析拿到 IP,建立 TCP 连接(三次握手),如果是 HTTPS 还要做 TLS 握手协商密钥,然后客户端按约定格式发出请求报文,服务端处理完返回响应报文,浏览器解析渲染,最后根据连接策略决定是复用还是关闭连接。这个流程面试里经常被打断追问,比如“为什么要三次握手不是两次”,那属于传输层的内容,这里不展开。

跟本文关系更紧的是最后一步:连接是复用还是关闭。HTTP/1.0 默认每个请求建一条连接,响应完就关,效率很低;HTTP/1.1 默认持久连接,通过Connection: keep-alive保持,服务端通常还会配置空闲超时,比如 60 秒没有新请求就主动关闭。这个超时时间在排查“偶发连接重置”类问题时非常关键——客户端连接池里的空闲连接如果比服务端的超时时间还长,就会拿到一条已经被对面关掉的连接,一发请求就报错。

1.4 版本演进用一句话概括每一代解决了什么

面试问版本差异,不需要背特性清单,抓住“每代解决什么问题”就够了。

版本传输层核心改进遗留问题
HTTP/1.0TCP定义了基本请求响应模型短连接,每个请求一次握手
HTTP/1.1TCP持久连接、Host 头、分块传输、缓存字段完善应用层队头阻塞,同域并发受限
HTTP/2TCP二进制分帧、多路复用、头部压缩TCP 层队头阻塞仍在
HTTP/3QUIC(UDP)0-RTT 建连、连接迁移、彻底解决队头阻塞部署与中间设备兼容成本

HTTP/1.1 的管道化(pipelining)是个常被提起又常被误解的东西:理论上客户端可以不等响应就连发多个请求,但响应必须严格按请求顺序返回,前一个慢就全堵住,加上中间设备兼容性差,浏览器基本都关掉了。这正是 HTTP/2 要解决的核心痛点——用二进制分帧把每个请求拆成带流 ID 的帧,在一条连接上交错发送,接收端按流 ID 重组,从而实现真正的并行。

2. 报文:从第一行就能读出整个请求的意图

2.1 请求报文的四段式结构

一个标准的 HTTP/1.1 请求报文长这样:

POST /api/v1/users?source=web HTTP/1.1 Host: api.demo.local Content-Type: application/json; charset=utf-8 Content-Length: 42 Connection: keep-alive {"name":"A","age":18}

结构上分成四段:请求行、首部字段、一个空行、消息体。请求行又由三部分组成——方法(POST)、请求目标(/api/v1/users?source=web)、协议版本(HTTP/1.1),三段之间用单个空格分隔。请求目标有三种常见形态:最常见的是 origin-form,也就是路径加查询串;经过正向代理时可能是 absolute-form,即完整的 URL;CONNECT 方法用的是 authority-form,只有主机和端口。这三种形态被问到的概率不高,但答出来能体现你看过规范。

首部字段是一堆名称: 值的键值对,每行一个,名称大小写不敏感(HTTP/2 里强制全小写),值一般区分大小写。空行是必须的,它标志着首部结束、消息体开始——这一点在做协议解析或者手写请求时特别重要,少了这个空行,服务端会把消息体的内容当成首部继续解析,直接报 400。

2.2 响应报文的形态与状态行三段

响应报文的结构和请求对称,只是第一行换成了状态行:

HTTP/1.1 200 OK Content-Type: application/json Content-Length: 27 Cache-Control: no-cache {"ok":true,"id":"1001"}

状态行由协议版本、状态码、原因短语三部分组成。原因短语是给人看的,客户端不应该依赖它做逻辑判断,因为它可以是任意文本,甚至可以为空。我见过有人写爬虫时用if "OK" in status_line判断成功,结果对方服务端把原因短语改成了中文,直接翻车。判断成功与否只看三位数字状态码,这是硬规矩。

响应报文不一定有消息体。状态码 204、304 以及所有对 HEAD 请求的响应,都不应该带消息体。这个规则听起来很细,但实际踩坑的人不少——某些框架里给 204 响应塞了一个 JSON,本地测试没问题,上了某些客户端或者经过 CDN 之后就会出现解析错位,因为接收方按规范认为没有 body,多出来的字节会被当成下一个响应的开头。

2.3 空行、CRLF 与消息体的边界判定

HTTP/1.x 是文本协议,行结束符规定是 CRLF,也就是回车加换行两个字节。整个首部区域的结束标志是连续两个 CRLF,即一个空行。这个细节在手工构造请求、写抓包解析脚本、配置网关规则时都用得上。

消息体的长度怎么确定,规范给了几种方式,优先级要记清楚。第一种是Transfer-Encoding: chunked,分块传输,每块前面用十六进制标明长度,最后用一个长度为 0 的块结束;第二种是Content-Length,明确给出字节数;第三种是连接关闭,HTTP/1.0 里没有这两个头时,服务端发完数据直接关连接来表示结束。前两种同时出现是明确禁止的,但现实中总有人这么干,而不同的中间件对这种畸形报文的处理方式不一致,很容易导致请求被错误切分,这也是很多安全加固规范里要求网关严格校验这两个字段的原因。

2.4 分块传输和内容压缩各自解决什么问题

Transfer-Encoding: chunked的核心价值是“不知道最终长度也能边生成边发”。典型场景是服务端动态渲染一个大页面,或者做流式输出,这时候没法提前算出Content-Length,用分块就能先把响应头发出去,再一块一块吐数据。格式大致如下,每块是“十六进制长度 + CRLF + 数据 + CRLF”,最后以0加空行收尾:

HTTP/1.1 200 OK Content-Type: text/plain Transfer-Encoding: chunked 7\r\n Hello, \r\n 6\r\n world!\r\n 0\r\n \r\n

注意这跟Content-Encoding: gzip是两个层面的东西:前者是传输层面的切块,后者是内容层面的压缩。两者可以同时存在,顺序是先压缩再分块。面试里问“chunked 和 gzip 有什么区别”,答“一个是传输编码,一个是内容编码,作用层级不同”就够准确了。

2.5 手工发一个请求,比看十篇文章都管用

想真正理解报文,最快的办法是拿终端自己发一个。用nc或者telnet都行:

printf 'GET / HTTP/1.1\r\nHost: demo.local\r\nConnection: close\r\n\r\n' | nc demo.local 80

注意\r\n一个都不能少,尤其是最后那两个。我第一次干这事的时候只写了\n,服务端一直不返回,排查了半小时才发现是行结束符不符合规范。如果想看响应头和响应体的完整内容,用 curl 更方便:

curl -v -X POST https://api.demo.local/api/v1/users \ -H 'Content-Type: application/json' \ -d '{"name":"A","age":18}'

-v会把请求行、请求头、响应行、响应头全部打出来,加上--trace-ascii -甚至能看到逐字节的传输过程。这个习惯我保持了几年,遇到任何接口行为不符合预期,先-v看一眼实际发出去的是什么,比在代码里反复加日志快得多。

3. 请求方法:安全和幂等才是真正的考点

3.1 九个方法先列清楚,再谈语义

规范里定义的方法主要有这么几个,先有个全局印象:

方法是否安全是否幂等典型用途
GET是是查询资源,参数在 URL 上
HEAD是是只要响应头,探活、取元信息
OPTIONS是是探测支持的方法、跨域预检
POST否否创建资源、提交表单、触发动作
PUT否是全量替换目标资源
PATCH否不保证局部更新资源
DELETE否是删除资源
CONNECT否是建立隧道,多用于加密代理场景
TRACE是是回显请求,用于诊断,通常被禁用

“安全”这个词在 HTTP 语境里不是指加密,而是指“不改变服务端资源状态”,只读。GET、HEAD、OPTIONS 是安全的,浏览器和爬虫可以放心预取。幂等是指同一个请求执行一次和执行多次,对服务端资源产生的最终状态相同。注意幂等说的是“最终状态”,不是“返回结果”——DELETE 一个资源,第一次返回 204,第二次返回 404,状态码不同,但资源状态都是“已删除”,所以它仍然是幂等的。

3.2 为什么工程上这么在意幂等

因为网络是不可靠的。客户端发了一个请求,超时了,它不知道服务端到底处理没处理,这时候要不要重试?如果接口是幂等的,闭着眼睛重试;如果不幂等,重试就可能导致重复下单、重复扣款。这就是为什么支付类接口几乎都会要求传一个幂等键,服务端拿这个键做去重。缓存系统也依赖这套语义:GET 和 HEAD 可以被缓存、被 CDN 缓存、被浏览器预取,而 POST 默认不缓存。

网关和负载均衡器同样会利用这些语义做优化。有些网关会对幂等请求自动重试,对非幂等请求直接返回错误。我在一个项目里遇到过“偶发重复创建”的问题,最后定位到是客户端的 HTTP 库对 POST 做了超时重试,而服务端没做幂等键校验,一个用户点击变成了三条记录。这个坑的本质就是把非幂等的方法当成了可以随意重试的方法。

3.3 GET 和 POST 的六个常见误区

这道题被问烂了,但答得准的人不多。网上流传的很多说法其实是错的,逐条拆一下。

第一个误区是“GET 有长度限制”。HTTP 规范里从来没有规定过 URL 长度上限,所谓的两千多字符是浏览器和服务器的实现限制,比如某些老版本浏览器限制在 2KB 左右,常见 Web 服务器对单个请求头的限制在 8KB 量级。所以准确说法是:GET 的参数长度受限于浏览器和服务器实现,而不是协议本身。

第二个误区是“GET 不能带 body”。规范没有禁止,只是说了带 body 的 GET 没有定义语义,很多服务器会直接忽略或者拒绝。实践上不要这么干,但这不等于协议不允许。

第三个误区是“POST 一定不幂等”。幂等性是接口实现决定的,不是方法名决定的。一个设计良好的 POST 查询接口完全可以幂等,只是它不符合规范的约定俗成,缓存和重试机制不会按幂等对待它。

第四个误区是“GET 比 POST 安全”。这句话只在“参数会不会留在日志里”这个意义上成立——GET 参数在 URL 上,会被浏览器历史、服务器访问日志、中间层日志完整记录下来;POST 参数在 body 里,相对不容易被顺手记下来。但两者在明文传输上没有任何区别,真要安全只能上 HTTPS。

第五个是编码问题。GET 参数要经过 URL 编码,中文、特殊字符都要转义,而且不同实现对方括号、加号的处理还不完全一致;POST 表单和 JSON 各有各的编码规则,处理起来反而更明确。

第六个是缓存和可重复性。GET 请求可以被收藏、被刷新、被前进后退重放,用户按 F5 重发一次 GET 没什么后果;POST 重发时浏览器会弹一个“确认重新提交表单”的提示,就是因为知道它可能不幂等。

3.4 PUT、PATCH、DELETE 怎么选

PUT 的语义是“用请求体里的内容完整替换目标资源”。注意是完整替换,不是合并。如果资源原本有 name、age 两个字段,你 PUT 只传了 name,那 age 就应该被删掉。这个语义很多人实现错了,做成了“存在则更新、不存在则插入”的合并逻辑,虽然用起来方便,但不符合 PUT 的定义。

PATCH 是局部更新,只改请求体里提到的字段。它不保证幂等,因为补丁本身可以带操作指令,比如“给某个数值加一”,执行两次结果就不一样。实际项目里 PATCH 用得不多,很多团队图省事全部用 POST 加一个 action 参数,这也是一种务实的选择,只要团队内部约定清楚、文档写明白就行。

DELETE 的幂等性前面提过,重点在于“删除不存在的资源”该返回什么。有团队返回 200 加一个{"deleted":0},有团队返回 404。从规范角度两种都能自圆其说,但从幂等性角度,返回 204 或者 200 更符合“执行多次结果一致”的直觉。真正需要小心的是软删除场景——如果你把 DELETE 实现成了“标记删除”,那第二次调用时资源已经标记过了,行为要明确,别出现“删两次扣两次额度”这种事。

3.5 HEAD 和 OPTIONS 在实际项目里的用途

HEAD 请求的响应和 GET 完全一样,只是不带消息体。它的价值在于“我只要元信息,不要内容”。常见用法有三种:一是探活,定时发一个 HEAD 看服务是否还活着,比 GET 省流量;二是提前拿Content-Length,做下载进度条;三是判断资源是否更新,比对ETag或Last-Modified。

这里有个细节值得提:某些服务端框架对 HEAD 请求的支持是半残的,路由只注册了 GET,HEAD 过来直接 405。按规范,支持 GET 的资源应该同时支持 HEAD,实际实现里靠框架自动处理,但如果你自己写了底层的路由分发,记得手动补上。

OPTIONS 主要有两个用途。一个是探测资源支持哪些方法,响应里会带Allow: GET, POST, OPTIONS这样的头;另一个是跨域场景下的预检请求,浏览器在发送真正的跨域请求之前,会先发一个 OPTIONS 问服务端“我能不能用这个方法和这些头访问你”,服务端通过Access-Control-Allow-*系列响应头来授权。跨域调试时看到的那个“预检失败”,本质上就是 OPTIONS 请求没被正确处理,或者响应头没配对。

3.6 方法相关的坑,一个比一个真实

第一个坑是网关拦截。前面开头提到的 405 就是这个原因。很多企业网关、WAF 的默认策略只放行 GET 和 POST,PUT、DELETE、PATCH 需要额外开白名单。排查这类问题的正确姿势是先抓包确认请求到底有没有到应用层:如果应用日志里压根没有这条记录,那问题一定在中间层。

第二个坑是 HTML 表单只支持 GET 和 POST。这是历史原因,早期表单规范没跟上,导致很多后端接口为了配合前端表单,把删除、更新全写成了 POST。现在用前端框架发 AJAX 请求就没这个限制了,但遗留系统里依然到处是POST /deleteUser这种写法。

第三个坑是方法大小写。规范里方法名是区分大小写的,标准方法都是全大写。有些服务端实现比较宽容,做了大小写归一化,有些严格按字节比对,get和GET会被当成两个不同的东西,直接返回 501。写客户端的时候老老实实大写,别给自己找麻烦。

第四个坑是 405 和 501 的区别。405 是“方法认识,但这个资源不支持”,响应必须带Allow头告诉对方支持什么;501 是“这个方法我压根不认识或者没实现”。两者含义完全不同,别混用。

第五个坑是 TRACE 一般要关掉。它会原样回显请求内容,某些场景下可能被利用来读取敏感信息,主流服务器默认都是关闭的,面试被问到知道“默认禁用”这个结论就行。

4. 状态码:按“谁该负责”来分类记忆

4.1 五位分类的记忆逻辑

状态码是三位数字,第一位表示类别:1xx 信息性、2xx 成功、3xx 重定向、4xx 客户端错误、5xx 服务端错误。这个分类不是随便分的,它回答的是一个非常实用的问题——出问题了该找谁。看到 4xx,先去查请求参数、鉴权、路径;看到 5xx,先去查服务端日志、依赖服务、数据库。这个判断在跨团队协作时能省掉大量扯皮。

记忆的时候不要死背数字,按“场景”来记。比如 2xx 记住五种典型场景:正常成功、创建成功、已接受待处理、成功但无内容、部分内容;3xx 记住两类维度:永久还是临时、方法保不保持;4xx 记住“谁的问题”:没认证、没权限、找不到、方法不对、参数不对、太快了;5xx 记住“哪一层的问题”:代码崩了、网关转发出错、服务不可用、上游超时。这样记,考场上想不起来具体数字也能推。

4.2 2xx 里最容易被用错的几个

200 是最常见的,但它的含义只是“请求成功处理”,不代表业务成功。很多团队喜欢在 200 的响应体里塞{"code": 500, "msg": "失败"},从 HTTP 层面看这是成功的,从业务层面看是失败的。这种做法的好处是前端统一处理,坏处是监控系统、网关、CDN 全都以为一切正常,出问题时告警不响。我的建议是:能用状态码表达的错误就用状态码表达,业务层面的细分错误码放在响应体里,两者分工明确。

201 表示资源创建成功,规范建议带上Location头指向新资源的地址。实际项目里用的人不多,但如果面试官问“创建接口该返回什么”,答 201 比答 200 更专业。

202 表示请求已被接受但还没处理完,典型场景是异步任务。这个状态码的价值在于告诉客户端“别等了,去轮询或者等通知”。

204 表示成功但没有响应体。常用于 DELETE 成功、PUT 成功但无需返回内容。注意它的响应里不能有 body,也不该有Content-Length(或者只能是 0)。

206 表示部分内容,配合Range头使用,视频拖动进度条、大文件断点续传都靠它。响应里会带Content-Range: bytes 0-1023/10240这样的头,标明这次返回的是哪一段。

4.3 3xx 家族:四个重定向的差别必须门儿清

这是面试高频区,也是实际踩坑重灾区。四个状态码的差异集中在两个维度上:

状态码永久/临时是否保持原方法典型场景
301永久历史上多数实现会改成 GET域名迁移、HTTPS 升级
302临时历史上多数实现会改成 GET临时跳转、登录后跳回
303临时明确要求改成 GETPOST 后跳转到结果页
307临时保持原方法和 body需要保方法的临时跳转
308永久保持原方法和 body需要保方法的永久跳转

301 最危险的地方在于“永久”两个字。浏览器会缓存这个跳转,而且缓存时间可以很长,一旦配错,用户端可能几个月都跳不回来,你改了服务端配置也没用,因为请求根本没发出来。做域名迁移之前一定要确认跳转规则,最好先用 302 观察一段时间。

302 的历史包袱很重。规范原本要求保持方法,但早期浏览器实现成了“一律改用 GET”,后来为了兼容就固化下来了。所以一个 POST 请求收到 302 之后,大多数浏览器会用 GET 去访问新地址,body 丢了。如果你的接口依赖这个行为,那是撞对了;如果不想丢 body,就得用 307。

303 是明确定义“去用 GET 拿另一个资源”,语义最清晰,适合 POST 提交之后跳转到结果页,避免用户刷新时重复提交。

307 和 308 是后来补充的,专门解决“保持方法”的需求。区别只在于临时和永久。实际项目中 307 用得少,因为大部分跳转场景确实希望变成 GET,但做 API 网关或者服务迁移时,307/308 就很重要了。

4.4 4xx:分清是谁的锅,以及锅的哪一种

401 和 403 是最容易混的一对。401 是“未认证”,意思是“我不知道你是谁,请先出示凭证”,响应里应该带WWW-Authenticate头告诉客户端用什么方式认证。403 是“已认证但无权限”,意思是“我知道你是谁,但你不能访问这个资源”。判断口诀:需要登录却没有效凭证是 401,登录了但角色不对是 403。

400 和 422 也常被混。400 是请求本身有问题,比如 JSON 格式错误、缺少必填参数、类型不对。422 是语义错误,请求格式没问题但内容通不过业务校验,比如“年龄填了 -1”。422 严格来说来自 WebDAV 扩展,但在 REST 风格的接口里被广泛采用。实际项目里两个都用 400 也不会有人挑刺,但如果面试官问“参数校验失败该返回什么”,能说出 400 和 422 的区别会显得更有经验。

404 和 410 的关系是“现在不存在”和“曾经存在但现在没了”。410 在实际项目里几乎见不到,知道概念即可。

429 是限流,响应里通常带Retry-After告诉客户端多久之后重试。做开放平台或者有防刷需求的接口,这个状态码应该用起来,比返回 200 加一段“请求过于频繁”的文字要规范得多。

还有几个值得知道但不用背的:405 方法不允许(必须带 Allow 头)、406 无法满足 Accept 要求、408 请求超时、409 冲突(乐观锁版本号不匹配常用它)、413 请求体过大、414 URI 过长、415 不支持的媒体类型、416 范围不满足、431 请求头过大。

4.5 5xx:区分“我的代码挂了”和“我的上游挂了”

500 是最笼统的服务端错误,代码抛了未捕获异常一般就是它。生产环境不要把堆栈信息直接返回给客户端,但一定要记到日志里,并且带上一个追踪 ID,方便定位。

502 是网关错误,含义是“我作为网关,从上游服务器收到了无效响应”。常见原因是对面进程挂了、端口没开、返回的响应格式不对、连接被重置。503 是服务暂时不可用,通常是过载或者正在维护,可以带Retry-After告诉客户端什么时候再来。

504 是网关超时,含义是“我等上游响应等到超时了”。502 和 504 的区别很实用:502 通常说明上游有问题(挂了、拒连),504 通常说明上游太慢(处理超时)。排查方向完全不同,一个是去看上游进程活没活,一个是去看上游哪个环节慢。

这里补一个工程经验:监控告警一定要按状态码分类。5xx 上升通常是服务端问题,要立即介入;4xx 上升可能是客户端版本发布、参数变更、或者被扫接口,优先级不同。混在一起看,很容易被大量 404 扫描流量淹没真正的故障信号。

5. 首部字段:真正决定行为的部分

5.1 字段的语法规则和分类方式

首部字段的格式是名称: 值,名称大小写不敏感,冒号后面建议有一个空格(不规范也能解析),值的前后空白会被去掉。同一个字段可以出现多次,是否允许合并取决于字段本身,比如Set-Cookie就不能简单合并。

按作用范围分,有通用首部(请求响应都能用,比如Cache-Control、Connection)、请求首部(Host、Accept、Cookie)、响应首部(ETag、Location、Set-Cookie)、实体首部(描述消息体,比如Content-Type、Content-Length)。这个分类不用背得很死,面试时能说出几个典型字段属于哪类就够了。

真正需要掌握的是“哪些字段会改变行为”。下面挑几组最常用的展开说。

5.2 内容协商:Accept 系列和 Content-Type

内容协商要区分方向。请求头里的Accept、Accept-Charset、Accept-Encoding、Accept-Language是客户端在说“我能接受什么”,响应头里的Content-Type、Content-Encoding、Content-Language是服务端在说“我给的是什么”。

Content-Type用的是 MIME 类型,后面可以带参数,最常见的是charset。下面几个在实际项目里天天见:

取值使用场景注意事项
application/jsonJSON 接口建议显式带charset=utf-8,虽然规范默认就是 UTF-8
application/x-www-form-urlencoded普通表单提交参数要 URL 编码,空格会变成加号
multipart/form-data文件上传、含文件的表单需要 boundary 分隔,不能手动拼
text/html网页不写 charset 时浏览器可能猜编码,容易乱码
application/octet-stream二进制下载浏览器会当成附件下载
text/event-stream服务端推送长连接流式输出常用

Accept-Encoding和Content-Encoding管的是压缩。客户端发Accept-Encoding: gzip, br,服务端如果支持就用 gzip 或 brotli 压缩响应体,并在响应头里标Content-Encoding: gzip。这里有个常见的排查场景:接口返回的内容在浏览器里正常,用脚本请求却是乱码,八成是脚本没处理压缩,或者手动设了Accept-Encoding却没解压。

5.3 长度与连接管理:三个字段决定报文怎么被切开

Content-Length表示消息体的字节数,注意是字节不是字符。中文一个字在 UTF-8 下占三个字节,算错了就会导致接收方读多或者读少,读少表现为响应被截断,读多表现为一直卡住等后续数据。

Transfer-Encoding: chunked前面讲过了,它和Content-Length是互斥的。这里补充一个实践要点:如果你的服务端开启了压缩,某些中间件会自动把Content-Length去掉并改成 chunked,因为压缩后的长度事先算不出来。这时候如果你在代码里手动设置过Content-Length,反而可能导致响应头冲突。

Connection控制连接是否复用,取值keep-alive或close。HTTP/1.1 默认是 keep-alive,所以通常不用显式写,但如果客户端明确要关闭连接,加上Connection: close可以让服务端发完就关,省一次等待超时。HTTP/2 里这个字段被禁止使用,因为连接复用的是协议内建行为。

5.4 缓存三件套:强缓存、协商缓存和 Vary

缓存是 HTTP 里最有工程价值的部分,也是最容易答出彩的地方。整体分两层,第一层是强缓存,浏览器直接用自己的本地副本,压根不发请求;第二层是协商缓存,浏览器发请求问服务端“我这份还能用吗”,能用就返回 304,不能用就返回新内容。

强缓存由Cache-Control控制,Expires是 HTTP/1.0 的旧方案,用绝对时间,受客户端时钟影响,现在只作为兜底。Cache-Control的常用指令:

指令含义
max-age=秒资源在多少秒内新鲜,可直接使用
s-maxage=秒只对共享缓存(CDN)生效,优先级高于 max-age
no-cache可以缓存,但每次使用前必须向服务端确认
no-store完全不缓存,连本地副本都不留
private只允许浏览器缓存,CDN 不许缓存
public允许任何中间层缓存
must-revalidate过期后必须验证,不能用过期副本凑合
stale-while-revalidate=秒过期后先给旧内容,同时后台去更新

no-cache和no-store的区别是面试常问的点,很多人答反。no-cache是“存但每次要验证”,no-store是“压根别存”。涉及敏感信息的响应应该用no-store。

协商缓存靠两组字段配合。一组是ETag和If-None-Match,服务端给资源算一个版本标识,客户端下次带着这个标识来问,服务端比对一致就返回 304,不一致就返回 200 加新内容。另一组是Last-Modified和If-Modified-Since,用最后修改时间,精度只到秒,而且文件内容没变但修改时间变了会误判为已更新。所以优先级上ETag高于Last-Modified。

Vary是个容易被忽略但很重要的字段,它告诉缓存系统“这个资源的缓存键要包含哪些请求头”。比如响应里写了Vary: Accept-Encoding,那 gzip 版本和非压缩版本就要分开缓存。如果接口会根据Accept-Language返回不同语言的内容却没写Vary,CDN 上就会出现中文用户看到英文页面的问题。这类问题排查起来特别费劲,因为本地直连服务端永远是对的。

顺带说一个调试经验:浏览器按 F5 刷新会跳过强缓存但带上协商缓存字段,Ctrl+F5 强制刷新会两边都跳过,直接拿新的。这就是为什么有时候“用户说刷新了还是旧的”,因为用户按的是 F5,协商缓存返回了 304。

5.5 身份、来源与转发链路相关的字段

Cookie和Set-Cookie是维持会话的核心。Set-Cookie有几个关键属性:Domain控制生效域名,Path控制生效路径,Expires或Max-Age控制生命周期(不写就是会话级,关浏览器就没了),Secure限制只走加密连接,HttpOnly禁止脚本读取(防 XSS 窃取),SameSite控制跨站请求是否携带(Strict、Lax、None,None 必须配合 Secure)。

Host是 HTTP/1.1 强制要求的字段,它的作用是支持一台服务器上部署多个站点——服务端根据 Host 决定用哪个配置、返回哪个网站的内容。少了 Host,服务端根本不知道该给你哪份内容,所以 HTTP/1.1 里缺 Host 会直接返回 400。

Origin和Referer都跟来源有关,但用途不同。Origin只包含协议、域名、端口,不带路径,主要用于跨域请求的判定,浏览器自动加,脚本不能改。Referer包含完整来源页地址,常用于统计和防盗链,可以配置Referrer-Policy控制发送粒度。有些场景需要隐藏来源,就要把策略设成no-referrer。

Authorization用来携带认证凭证,配合 401 响应里的WWW-Authenticate使用。注意它和 Cookie 是两套并行的机制,不要在一套里混用。

还有一类字段在多层转发时用于保留客户端真实地址,比如X-Forwarded-For和X-Real-IP,它们不是规范标准字段,但被广泛支持。这里要注意的是这些字段可以被客户端伪造,服务端不能无条件信任,要以最靠近自己的那一层转发写入的值为准。

5.6 几个能救命的实用字段

Range和Accept-Ranges支撑断点续传。客户端发Range: bytes=1024-2047,服务端返回 206 加对应片段,并在Content-Range里标明范围。视频网站拖动进度条就是这个机制,下载工具的“续传”也是。

Expect: 100-continue是个很实用的优化。客户端要传一个大文件,先发这个头问一句“我要发了,你接不接”,服务端回 100 Continue 再真正发 body,回 417 就省了这次上传。这个机制在弱网环境下能省不少流量,但支持它的客户端和服务端都不算多。

Retry-After配合 429 或 503 使用,告诉客户端多久之后重试,比让客户端自己瞎猜退避时间要友好。

Location出现在 3xx 和 201 响应里,前者是跳转目标,后者是新资源地址。Allow出现在 405 响应里,列出支持的方法。这两个字段如果漏了,客户端的自动化处理逻辑就没法写。

6. 从背八股到手上有活:验证与追问演练

6.1 用 curl 把每个知识点亲手验证一遍

光看不练记不住。下面这几个命令,建议挨个跑一次,跑完对报文的理解会上一个台阶:

# 看完整请求和响应 curl -v https://api.demo.local/api/v1/users # 只发 HEAD,看响应头 curl -I https://api.demo.local/api/v1/users # 自定义方法和头 curl -X PATCH https://api.demo.local/api/v1/users/1 \ -H 'Content-Type: application/json' \ -d '{"name":"B"}' # 跟随重定向,观察跳转链路 curl -L -v http://demo.local/old-path # 携带和保存 Cookie curl -b 'sid=abc123' -c cookies.txt https://api.demo.local/profile # 请求指定字节范围 curl -r 0-1023 -o part.bin https://api.demo.local/bigfile # 自动处理压缩 curl --compressed https://api.demo.local/api/v1/users

跑curl -L -v观察 301 和 302 的时候,注意看第二个请求用的是不是 GET,有没有带 body,这比看多少篇文章都直观。

6.2 抓包和浏览器面板里能看到什么

浏览器的开发者工具里,Network 面板的 Headers 标签能看到请求头、响应头、状态码、请求方法;Timing 标签能看到 DNS、连接、TLS、等待、下载各阶段耗时。排查“接口慢”的时候,先看是等待时间长(服务端处理慢)还是内容下载时间长(响应体大或者网络差),方向完全不同。

Response 标签看到的是解压后的内容,如果想看原始压缩数据,得用抓包工具。命令行抓包用tcpdump配合-A参数可以看明文内容,但 HTTPS 是加密的,只能看到 TLS 握手和连接过程。

还有一个实用技巧:在浏览器地址栏直接输入一个返回 JSON 的接口地址,浏览器会按默认渲染规则展示,这对快速验证接口是否可用非常方便,比打开调试工具还快。

6.3 面试官可能的追问链路,提前想清楚

HTTP 这部分面试的特点是从一个简单问题开始,一层一层往下钻,钻到你答不上来为止。提前把链路走一遍,比零散背知识点有用得多。

链路 A:从 GET 和 POST 的区别出发,问到长度限制的真实原因,问到幂等性的定义,问到为什么支付接口要幂等键,问到怎么设计一个幂等的创建接口(答案通常是用业务唯一键去重或者用请求 ID 做表)。

链路 B:从状态码 301 和 302 的区别出发,问到浏览器缓存 301 之后怎么恢复,问到 307 为什么能保持方法,问到前端发 POST 后跳转该用哪个码,最后问到 CDN 上配错了跳转怎么紧急止损。

链路 C:从缓存字段出发,问到强缓存和协商缓存的区别,问到Cache-Control和Expires的优先级,问到ETag和Last-Modified各自的缺陷,问到为什么线上会出现“用户看到旧页面”,最后问到怎么用Vary解决多语言站点串内容的问题。

链路 D:从报文结构出发,问到Content-Length和Transfer-Encoding冲突会怎样,问到跨域预检为什么是 OPTIONS,问到网关为什么拦 PUT,最后问到怎么用抓包定位请求卡在哪一层。

每一条链路都在考同一件事:你有没有把这些知识串成一个能用的模型。

6.4 三个我自己踩过的坑

第一个是 204 响应带了 body。某次重构时顺手复用了统一的响应包装,给 204 也塞了一个 JSON,本地所有测试通过。上线之后有合作方的客户端解析报错,因为他们严格按规范认为 204 没有 body,多出来的字节被当成了下一个响应的开头。修复很简单,给那几个接口换成了 200,或者干脆清空 body。教训是:规范里写“不应该”的地方,不要图省事。

第二个是Content-Length和压缩的冲突。一个下载接口手动设置了Content-Length为文件原始大小,中间又开了一层压缩,结果客户端收到的数据比预期少,下载总是失败。原因是压缩之后长度变了,但Content-Length还是原值,接收方按原值读完就以为结束了。正确做法是压缩开启时交给框架自动处理长度或者改用分块。

第三个是缓存把登录态串了。一个接口返回用户信息,响应头里写了Cache-Control: public, max-age=300,本意是想让 CDN 缓存减轻压力。结果多个用户拿到了同一个人的信息,因为缓存键里没有区分用户。修复方式是改成private或者干脆no-store,涉及个人数据的响应千万别用共享缓存。

最后分享一个准备面试的小习惯:把上面这几章当成一个索引,每看到一个知识点,就问自己“线上什么情况会用到它”。答不上来的,就去把对应的字段和状态码翻一遍。HTTP 这东西不难,难的是把它和你每天写的代码连起来——当你能说出“这个 bug 是因为网关不认 PUT”的时候,那些八股才真正变成你的东西了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询