# 1 HTTP 报文的组成部分
30 秒速记
- 请求报文:请求行(方法 + 路径 + 版本)→ 请求头 → 空行 → 请求体
- 响应报文:状态行(版本 + 状态码 + 原因短语)→ 响应头 → 空行 → 响应体
- 空行是头和体的分界;头里混进用户输入的换行符,就能伪造头部,叫响应拆分
- 头太大会被网关拒(
431),大多是cookie膨胀,静态资源放无cookie域名 HTTP/2起不再是纯文本:头部变HEADERS帧(HPACK压缩),请求行变成:method、:path伪头,正文是DATA帧
HTTP/1.x 的报文就是四段:起始行、头部、空行、正文,请求和响应结构一样,只是起始行不同。 请求的起始行是方法、路径和版本,响应的是版本、状态码和原因短语。那个空行很关键,头部长度不固定,服务端读到空行才知道头结束了。实际工作里跟报文打交道最多的是头部:cookie 塞多了会触发 431,跨域加自定义头会多一次预检。到了 HTTP/2,报文变成二进制帧,概念还是这几块,但抓包时就看不到一行行的文本了。
请求报文四部分:
POST /api/order HTTP/1.1 ← 请求行:方法 + 路径 + 协议版本
Host: api.example.com ← 请求头
Content-Type: application/json
Content-Length: 27
← 空行(关键分隔符)
{"id":123,"count":1} ← 请求体
响应报文四部分:
HTTP/1.1 200 OK ← 状态行:协议版本 + 状态码 + 原因短语
Content-Type: application/json ← 响应头
Cache-Control: no-cache
← 空行
{"code":0,"data":{}} ← 响应体
那个空行为什么重要?
因为 HTTP 头部是变长的 —— 服务端读到一个空行才知道"头结束了,后面是 body"。这也是为什么头部字段不能包含裸的换行符,否则就能伪造出一个假的分隔,这类攻击叫 HTTP 响应拆分。
实际工作中会遇到的几个细节:
① 头部太大会被拒绝。 常见于 cookie 膨胀 —— 每个同域请求都会带上全域 cookie,塞多了轻松几 KB,超过网关限制就返回 431 或直接被断开。所以静态资源该放独立的无 cookie 域名。
② 自定义请求头有代价。 跨域时会触发 OPTIONS 预检,等于每个请求变两个往返;中间的 CDN/网关也可能不透传未知的头。能放 body 就别放头里。
③ Content-Length 和分块传输二选一。 知道总长度就用 Content-Length;流式响应(SSE、大文件、AI 逐字输出)用 Transfer-Encoding: chunked,此时没有总长度,所以做下载进度条时算不出百分比。
④ HTTP/2 之后报文结构变了。 起始行和头部被压缩成 HEADERS 帧(用 HPACK 编码),body 变成 DATA 帧,不再是纯文本。所以严格说"报文由文本行组成"只适用于 HTTP/1.x——但概念层面的四部分划分仍然成立。
面试官追问
追问 1POST /api/order HTTP/1.1 这一行属于请求头还是请求体?
都不是,这是请求行,里面是方法、路径和协议版本。很多人只记得「头 + 体」,漏了起始行和空行,响应那边对应的是状态行 HTTP/1.1 200 OK。
追问 2请求体才几十字节,接口却偶发返回 431,查哪?
查请求头,基本是 cookie 太大。同域所有请求都会带上全部 cookie,埋点、A/B 实验各写几个就能到好几 KB。清理没用的 cookie,静态资源换无 cookie 域名,调网关上限只是缓解。
追问 3给每个接口加了个 X-Trace-Id 请求头,跨域接口明显变慢了,为什么?
自定义头会让跨域请求变成非简单请求,每次先发 OPTIONS 预检,等于多一个往返。服务端返回 Access-Control-Max-Age 让预检结果缓存,或者把追踪 ID 放到 URL 参数或请求体里。
追问 4后端改成流式下载后,前端进度条拿不到百分比了,是前端写错了吗?
不是。流式响应用 Transfer-Encoding: chunked,开始传的时候没有 Content-Length,前端拿不到总大小。要么后端额外在头里给总大小,要么前端只显示已下载多少。
追问 5HTTP/2 抓包时怎么找不到请求行了?
HTTP/2 把请求行拆成了 :method、:path、:scheme、:authority 几个伪头,和普通头一起放进 HEADERS 帧压缩传输。DevTools 里看到的文本是浏览器解码后展示的,线上实际字节是二进制帧。
# 2 常见状态码
30 秒速记
- 看首位分锅:
2xx成功、3xx重定向、4xx请求方有问题、5xx服务端有问题 401= 不知道你是谁(去登录),403= 知道你是谁但没权限(提示无权限);混着处理会把越权用户反复踢到登录页301/308永久,浏览器会缓存;302/307临时;307/308保证不改请求方法,301/302下POST可能被改成GET502= 上游返回了坏响应(服务挂了),504= 上游超时没回(处理太慢)- 常用还有
204无内容、206断点续传、304协商缓存命中、429限流(看Retry-After) - 别用
200+body.code表示失败,监控和网关只认状态码,CDN还可能把错误缓存下来
状态码先看首位分方向:4xx 先查自己的请求,5xx 找后端。 面试最常追的是三组容易混的:401 和 403,一个是没登录,一个是没权限,前端拦截器要分开处理;301 和 302,永久重定向会被浏览器缓存,临时活动配成 301 之后很难收回;502 和 504,一个是上游服务挂了,一个是上游太慢。还有一个工程建议:业务失败别全返回 200,不然监控看不到错误率,CDN 还可能把错误页缓存住。
五大类,看首位数字就知道谁的问题:
| 类别 | 含义 | 谁的责任 |
|---|---|---|
1xx |
信息性,请求已收到继续处理 | — |
2xx |
成功 | — |
3xx |
重定向 | — |
4xx |
客户端错误 | 前端/调用方 |
5xx |
服务端错误 | 后端 |
这个划分在排查时很实用:看到 4xx 先查自己的请求(参数、鉴权、方法),看到 5xx 直接找后端。
必须记住的这些:
| 码 | 含义 | 实际场景 |
|---|---|---|
200 |
成功 | — |
201 |
已创建 | POST 创建资源后返回,比笼统的 200 语义好 |
204 |
成功但无内容 | DELETE 成功、OPTIONS 预检响应 |
206 |
部分内容 | 断点续传、视频拖动进度条(配 Range 头) |
301 |
永久重定向 | 域名迁移,会传递 SEO 权重,浏览器会硬缓存 |
302/307 |
临时重定向 | 活动页跳转;307 保证不改变请求方法 |
304 |
未修改 | 协商缓存命中,直接用本地副本 |
400 |
请求语法错误 | 参数格式不对 |
401 |
未认证 | 没登录或 token 过期 → 跳登录页 |
403 |
已认证但无权限 | 登录了但角色不够 → 提示无权限,别跳登录 |
404 |
资源不存在 | — |
405 |
方法不允许 | 用 GET 调了只支持 POST 的接口 |
413 |
请求体过大 | 上传超限 |
429 |
请求过于频繁 | 被限流,配合 Retry-After 头做退避重试 |
500 |
服务端内部错误 | — |
502 |
网关收到上游无效响应 | 后端服务挂了或还没起来 |
503 |
服务不可用 | 过载或维护中 |
504 |
网关超时 | 后端处理太慢 |
三组最容易混的:
① 401 vs 403 —— 401 是"你是谁我不知道"(该跳登录),403 是"我知道你是谁但你不能干这个"(该提示无权限)。前端拦截器把两者混在一起处理,会导致越权用户被反复踢到登录页。
② 301 vs 302 —— 301 会被浏览器硬缓存,之后连请求都不发。临时跳转误用 301,活动结束后老用户依然被强制跳走且清不掉,只能换新路径。
③ 502 vs 504 —— 502 是上游返回了无效响应(服务挂了),504 是上游根本没在超时前返回(处理太慢)。区分开能直接告诉后端往哪个方向查。
一个工程上的强建议:不要用 200 + body 里的 code 表示业务失败。监控和网关只看 HTTP 状态码,全是 200 就发现不了故障;CDN 还可能把错误响应当成功缓存下来。正确做法是 HTTP 状态码反映请求本身的结果,业务细分错误码放 body。
面试官追问
追问 1拦截器里 401 和 403 都跳登录页,普通员工点进管理页就一直在循环,怎么改?
401 清 token 跳登录;403 保留登录态,弹「无权限」或跳到 403 页面。用户重新登录也还是没权限,跳登录页解决不了问题。
追问 2活动页临时跳转误配成 301,改回 302 后老用户还是被跳走,为什么?
301 默认会被浏览器缓存,很多用户之后根本不发请求,直接本地跳了。服务端改配置管不到他们,只能等缓存过期或者换一个新路径。临时跳转一律用 302 或 307。
追问 3表单 POST 提交后后端返回 302 到另一个接口,结果那边收到的是 GET,怎么回事?
历史原因,浏览器遇到 301 / 302 会把 POST 改成 GET 再请求。要保留方法和请求体就用 307(临时)或 308(永久)。
