# HTTP的实体数据
30 秒速记
- 实体数据(现在规范里叫「表示」,
representation)就是body,要让对方处理对,得说清四件事 - 是什么:
MIME类型,Accept↔Content-Type - 怎么压缩:
Accept-Encoding↔Content-Encoding - 什么语言:
Accept-Language↔Content-Language;字符集请求用Accept-Charset,响应写在Content-Type的charset里 - 协商规则:客户端列候选 +
q权重,服务器挑一个并用Content-*告诉你挑了啥;结果随请求头变的要加Vary
实体数据就是报文的 body,光有一串字节没用,还得告诉对方它是什么、压没压缩、什么语言、什么字符集。 HTTP 用两组头来对齐这四件事:客户端用 Accept 开头的请求头说「我能接受什么」,服务器用 Content 开头的响应头说「我实际给了什么」,这个过程叫内容协商。比如浏览器发 Accept-Encoding: gzip, br,服务器选了 br 就回 Content-Encoding: br。字符集是个特例,响应里没有 Content-Charset,而是写成 Content-Type: text/html; charset=utf-8。同一个地址如果会按这些请求头返回不同版本,要加 Vary,不然 CDN 可能把压缩版发给不支持的客户端。
HTTP 报文由起始行、头部和 body 三块组成,body 就是这里说的实体数据。早期规范(RFC 2616)叫它 entity,现在的 RFC 9110 改叫 representation,也就是「资源的某一种表示」:同一篇文章可以有 HTML 版和 JSON 版、中文版和英文版、压缩版和原始版,它们都是同一个资源的不同表示。
服务器要挑出最合适的那一个表示,就靠内容协商。四个维度一一对应:
| 维度 | 请求头(我能接受) | 响应头(我实际给的) |
|---|---|---|
| 数据类型 | Accept |
Content-Type |
| 压缩方式 | Accept-Encoding |
Content-Encoding |
| 自然语言 | Accept-Language |
Content-Language |
| 字符集 | Accept-Charset |
Content-Type 里的 charset |
一次典型的交换长这样:
GET /api/article/42 HTTP/1.1
Accept: application/json, text/html;q=0.8
Accept-Encoding: gzip, br
Accept-Language: zh-CN, en;q=0.5
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Encoding: br
Content-Language: zh-CN
Vary: Accept, Accept-Encoding, Accept-Language
浏览器拿到响应后先按 Content-Encoding 用 br 解压,再按 Content-Type 当成 UTF-8 编码的 JSON 解析。Vary 告诉中间的缓存:这三个请求头不同,拿到的可能是不同版本,不能混用。现实中浏览器很少发 Accept-Charset,服务端也常省略 Content-Language,最常见的组合是请求带 Accept、Accept-Encoding、Accept-Language,响应带 Content-Type、Content-Encoding。
面试官追问
追问 1接口返回的 body 前端解析失败,第一步看什么?
先看响应头:Content-Type 是不是 application/json,Content-Encoding 有没有、和实际字节对不对得上,charset 是不是 utf-8。浏览器 Network 面板的 Response 和 Headers 对着看,大部分问题在这一步就能定位。
追问 2Content-Type 和 Content-Encoding 都有 Content 前缀,怎么分清?
Content-Type 说内容本身是什么,比如 application/json;Content-Encoding 说传输前额外做了什么压缩,比如 gzip。浏览器先按 Encoding 解压,再按 Type 解析,顺序不能反。
追问 3中文乱码,后端说已经加了 Content-Language: zh-CN,还缺什么?
缺 charset。Content-Language 只说内容是中文,不管字节怎么解码。要写 Content-Type: text/html; charset=utf-8,并且实际字节真的是 UTF-8。
追问 4前端能不能自己在 fetch 里设 Accept-Encoding?
不能,它是浏览器的禁止修改头,设了也会被忽略,浏览器会按自己的能力填。前端能控制的是 Accept、Accept-Language 这类,比如 fetch(url, { headers: { Accept: 'application/json' } })。
追问 5同一个 URL 按语言返回中英文两个版本,CDN 偶尔给中文用户返回英文,为什么?
响应没带 Vary: Accept-Language,CDN 只按 URL 当缓存键,谁先访问就缓存谁的版本。补上 Vary,同时确认 CDN 真的按这个头区分缓存;更稳的做法是把语言放进 URL,比如 /en/ 和 /zh/,对 SEO 也更友好。
# 数据类型与编b码
30 秒速记
MIME type= 「大类/小类」,告诉接收方按什么解析:text/html、image/png、application/json- 五大类:
text、image、audio、video、application(要靠应用解释,JSON、PDF、JS都在这) application/octet-stream= 「不知道是啥的二进制」,浏览器通常直接当下载处理Encoding type只管压缩:gzip最通用,deflate用得少,br(Brotli)压得更小,浏览器只在HTTPS下声明支持- 压缩不改变内容类型:压缩过的
PDF还是Content-Type: application/pdf+Content-Encoding: gzip
MIME type 说「这是什么」,Encoding type 说「它被怎么压缩过」,两个信息缺一不可。 浏览器看到 text/html 就交给渲染引擎,看到 image/png 就当图片显示,看到 application/octet-stream 就不知道是啥了,一般直接弹下载。压缩是另一层:为了省带宽,服务器可能把内容用 gzip 或 br 压一下,这时候要额外告诉对方用的哪种算法,不然浏览器拿到的是一堆解不开的字节。我常提醒的一点是,压缩不改变内容的本质,HTML 压完还是 text/html,只是多一个 Content-Encoding: gzip。
但仅有
MIME type还不够,因为HTTP在传输时为了节约带宽,有时候还会压缩数据,为了不要让浏览器继续“猜”,还需要有一个“Encoding type”,告诉数据是用的什么编码格式,这样对方才能正确解压缩,还原出原始的数据。
比起 MIME type 来说,Encoding type 就少了很多,常用的只有下面三种
gzip:GNU zip压缩格式,也是互联网上最流行的压缩格式;deflate:zlib(deflate)压缩格式,流行程度仅次于gzip;br:一种专门为HTTP优化的新压缩算法(Brotli)
面试官追问
追问 1预览接口返回 application/octet-stream,能不能直接塞给 <img> 显示?
有时候能显示,因为浏览器对图片会嗅探字节头,但这是碰运气。正确做法是后端按实际格式返回 image/png、image/jpeg,要下载再配 Content-Disposition: attachment。
追问 2PDF 下载加了 gzip 压缩,后端想把类型改成 application/gzip,对吗?
不对,那样客户端解压后不知道该当 PDF 处理。传输压缩用 Content-Encoding: gzip,类型保持 application/pdf
