Content-Type 说明正文是什么媒体类型,必要时带上 charset。
它决定浏览器是渲染、解析,还是弹出下载框——写错一个词,用户看到的东西就全变了。
前面几个字段都在描述正文的数量、位置、语言,这一节说正文到底是什么。 它是实体首部里最常用、也最容易被随手写错的一个。
一、格式是「类型/子类型」加参数
text/html; charset=utf-8 分两段:text/html 是媒体类型,charset=utf-8 是参数。
参数用分号分隔,值可以带引号。
Demo 里五种类型各发一次,看整条响应多大:text/html 111 字节、application/json 101 字节、
image/png 131 字节、text/plain 190 字节、application/octet-stream 204 字节。
注意 image/png 那行:正文才 48 字节,整条响应 131 字节,首部占了六成多。
小响应里首部的开销就是这么显眼。
二、charset 什么时候必须写
正文里有非 ASCII 字符时必须写,而且要跟实际编码一致。
少了它,浏览器只能猜——过去猜 GBK 的浏览器会把 UTF-8 中文显示成乱码。
注意 charset 说的是编码(UTF-8、GBK),不是字符集(Unicode)。
这两个词在 HTTP 里一直被混用,但参数值必须是编码名。
还有一条:Content-Type 里的 charset 优先级高于页面里 <meta charset>。
两者不一致时以首部为准,所以改编码要改首部,光改 meta 没用。
三、它决定浏览器怎么处理
浏览器拿到响应,先看 Content-Type,再决定怎么做:text/html 渲染成网页,
application/json 交给脚本解析,image/png 交给图片解码器。
application/octet-stream 是兜底:「我不认识,别渲染,存成文件」。
很多人下载 exe 时弹出的警告,走的就是这条路。
反过来说,能渲染的东西别乱标成它,否则用户看到的是下载框而不是页面。
安全上有两个经典后果:把用户上传的 HTML 标成 text/html,等于给他一个 XSS 入口;
标成 application/octet-stream 加 Content-Disposition: attachment,浏览器就只会下载不会执行。
四、动手 Demo:五种类型,五种下场
同一台服务端,同一个请求路径,只改 Content-Type 和正文,看整条响应大小怎么变、
浏览器会采取什么行为。
cases = [
(u'text/html; charset=utf-8', HTML_BYTES),
(u'application/json', JSON_BYTES),
(u'image/png', PNG_BYTES),
(u'text/plain', TEXT), # 没写 charset
(u'application/octet-stream', TEXT), # 兜底类型
]
def fn(raw, st, _ct=ct, _b=body):
return X.reply(u'HTTP/1.1 200 OK', [(u'Content-Type', _ct)], _b)
实跑输出:
--------------------------------------------------------------------
Content-Type:正文到底是什么
--------------------------------------------------------------------
Content-Type 正文 整条响应 浏览器会怎么做
text/html; charset=utf-8 13 111 按网页渲染
application/json 11 101 按 JSON 解析
image/png 48 131 按图片解码
text/plain 105 190 纯文本,编码靠自己猜
application/octet-stream 105 204 不认识的二进制,多半触发下载
两列数字值得对照:`image/png` 那行正文 48 字节,整条响应 131 字节,
光首部就占了大半。这就是为什么小响应要合并、首部要压缩。
再看 `text/plain` 那行:没有 charset。中文正文缺了它,
浏览器只能猜——过去猜 GBK 的浏览器会把 UTF-8 的中文显示成乱码。
所以只要正文里有非 ASCII 字符,charset 就必须写,而且要跟实际编码一致。
最后一行 `application/octet-stream` 是个兜底:
浏览器不认识它,规矩就是「别渲染,存成文件」。很多人下载 .exe 弹警告,
用的就是这条路。反过来说,能渲染的东西别乱标成它,不然用户看到的是下载框。
五行正文长度:13、11、48、105、105;整条响应:111、101、131、190、204。
image/png 那行最能说明问题:48 字节正文撑出 131 字节响应,首部比正文还长。
这是小资源要合并、首部要压缩的直接原因。
text/plain 那行没写 charset。同样的字节,浏览器可能猜 GBK 也可能猜 UTF-8,猜错就是乱码。
加上 charset=utf-8 就多十几字节,但省掉一堆投诉。
最后一行 application/octet-stream 是明确告诉浏览器「别猜了,存下来」。
这个决定权在你手上,不在用户手上。
30 秒实验
- 把 UTF-8 正文标成
charset=gbk,看解出来是什么。 - 把 HTML 标成
application/octet-stream,想想要怎么才能让它在浏览器里显示。 - 想一想:为什么用户上传的文件不能按原 Content-Type 直接回给别的用户?
常见误区
- 「charset 可以省,浏览器会自己猜」——能猜,但猜错就是乱码。
- 「
text/plain里的中文也能正常显示」——不写 charset 就可能乱码。 - 「Content-Type 只是给浏览器看的提示」——它决定渲染、解析还是下载。
自测三问(点开对答案)
1. `text/html; charset=utf-8` 里两段各是什么? —— `text/html` 是媒体类型,`charset=utf-8` 是编码参数。 2. `Content-Type` 里的 charset 和页面 `` 冲突,听谁的? —— 听首部的。 3. `application/octet-stream` 会让浏览器怎么做? —— 不渲染,当作二进制存成文件,通常触发下载。类型说完了。接下来两个是缓存的老朋友:Expires 和 Last-Modified。
- ☐ (本节完,下一节 6.6.9 Expires)