大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
请求与响应头
这一课你将能做到
- 解释 Header 是「键值对元数据」,分清请求头与响应头各自管什么、往哪个方向说话
- 亲手设置并读懂 Host、Accept、Content-Type、Authorization、Cache-Control、ETag、Set-Cookie 的常见用法,并用 curl
-H复现一条浏览器请求 - 在 DevTools Headers 面板与 curl 之间互转同一条请求,并指出 Nginx 反代会追加/改写哪些头
为什么要学
同一条 GET /api/tracks:没有 Content-Type 约定,后端可能把 JSON 当表单解析直接 415;没有 Authorization,管理接口只会 401;没有 Cache-Control,用户天天看旧数据;没有 CORS 相关头,前端跨域全军覆没。缓存、CORS、Cookie、压缩、内容协商——本 track 后面一半的内容全是头在说话。
面试里「常见请求头响应头有哪些」看似送分,实际考察你有没有真的排查过问题:答得出 X-Forwarded-For 的作用、Vary 的坑,说明摸过生产。本课把头当「便签」建立直觉:头是元数据,body 是包裹内容。
先建立图像
请求信封
请求行:GET /api/tracks HTTP/1.1
├─ Host / User-Agent / Accept / Authorization … ← 请求头:客户端说的话
└─(空行)
└─ body(POST/PUT 才常有)
响应信封
状态行:HTTP/1.1 200 OK
├─ Content-Type / Set-Cookie / ETag / Cache-Control … ← 响应头:服务器说的话
└─(空行)
└─ body
高频头速查表:
| 头 | 方向 | 人话 | 示例 |
|---|---|---|---|
| Host | 请求 | 我要访问哪个站点(虚拟主机路由) | Host: coderoadmap.cn |
| Accept | 请求 | 我希望收到什么格式 | application/json |
| Content-Type | 双向 | 本侧 body 是什么格式 | application/json;charset=UTF-8 |
| Authorization | 请求 | 我的凭证 | Bearer eyJhbGci… |
| Cookie | 请求 | 浏览器自动带的小票 | satoken=abc123 |
| Set-Cookie | 响应 | 请浏览器存这张小票 | 见 Cookie 课 |
| Cache-Control | 响应为主 | 能不能缓存、缓存多久 | max-age=60 |
| ETag | 响应 | 内容指纹(版本号) | "a1b2c3" |
| If-None-Match | 请求 | 我本地是这个版本,变了没 | 配合 ETag → 304 |
| User-Agent | 请求 | 我是什么客户端 | Mozilla/5.0 … |
| X-Forwarded-For | 请求(代理追加) | 原始客户端 IP 链 | 1.2.3.4, 10.0.0.2 |
跟着做
步骤 1:只看响应头
# -I 只取头(发 HEAD 请求)
curl -sI https://coderoadmap.cn/api/tracks
# 本机对照
curl -sI http://localhost:8080/api/hello
记录三件事:content-type 是什么;有没有 cache-control / etag;有没有 set-cookie。这三个答案决定了这条响应能不能缓存、怎么被解析、是否影响会话。
步骤 2:Accept 与内容协商——「期望」不等于「结果」
# 明确说我要 JSON
curl -i -H "Accept: application/json" https://coderoadmap.cn/api/tracks
# 偏要 HTML(观察是否仍返回 JSON)
curl -i -H "Accept: text/html" https://coderoadmap.cn/api/tracks
多数纯 API 忽略 Accept 永远回 JSON——内容协商是双方约定,不是魔法。Spring MVC 的 @RestController 默认按 produces 与路径后缀协商;若启用 @EnableWebMvc + 内容协商配置,行为才严格跟随 Accept。
步骤 3:POST 的 Content-Type 是接口联调第一坑
# 正确:声明 JSON,Spring @RequestBody 才能反序列化
curl -i -X POST http://localhost:8080/api/notes \
-H "Content-Type: application/json" \
-d '"hello"'
# 错误示范 1:不声明(curl 默认 application/x-www-form-urlencoded)
curl -i -X POST http://localhost:8080/api/notes -d '"hello"'
# 错误示范 2:声明成表单但 body 是 JSON
curl -i -X POST http://localhost:8080/api/notes \
-H "Content-Type: application/x-www-form-urlencoded" -d '{"a":1}'
错误示范会得到 415 Unsupported Media Type。必须记住的三类:
application/json —— REST API
application/x-www-form-urlencoded —— 传统表单键值对
multipart/form-data —— 文件上传
步骤 4:Authorization 与 Cookie 谁在起作用
# Bearer 方式:头是显式的
curl -i http://localhost:8080/admin/tracks -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxx"
# Cookie 方式:头是浏览器自动带的(curl 要手动模拟)
curl -c jar.txt http://localhost:8080/api/auth/login -X POST \
-H "Content-Type: application/json" -d '{"username":"u","password":"p"}'
curl -b jar.txt -i http://localhost:8080/admin/tracks
两种都是「把凭证放进请求头」,差别在于 Cookie 是浏览器按存储规则自动附加,Authorization 是代码显式设置(Cookie 课对比两者)。
步骤 5:DevTools 与 curl 互转
- Network 面板 → 选
api/tracks→ Headers 分栏:上半 Request Headers,下半 Response Headers - 右键该请求 → Copy → Copy as cURL,粘贴到终端执行
- 对比你手写的极简 curl:浏览器多出的
sec-ch-ua、accept-language等是浏览器自动补的客户端提示
复现 bug 时不用全贴,保留关键头即可:Cookie、Authorization、Content-Type、Origin。
步骤 6:Nginx 反代改写了什么头(架构视角)
生产配置里常见这几行(对照本站 Nginx 模板):
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host; # 保留原始域名
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 追加真实客户端 IP 链
proxy_set_header X-Forwarded-Proto $scheme; # 告诉应用外层是 https
}
验证差异:curl -i http://localhost:8080/api/hello 直连 Spring,没有 X-Forwarded-*;走 Nginx 反代后(本机配一条 location /api/ 再 nginx -s reload)响应前日志里能看到应用收到了这些头。Spring 侧要开启对 Forwarded 头的信任,request.getRemoteAddr() / 判断 HTTPS 才是对的——否则限流按 Nginx 的 IP 限、Secure Cookie 判断错,都是这类配置的坑。
讲清楚要点
- 因为 Host 决定虚拟主机,所以同一台 Nginx 上挂多个站点全靠它分流——HTTP/1.1 里 Host 是必填头;缺了它,反代不知道把请求转给谁。
- 因为 Content-Type 描述「发送方自己的 body」,所以请求与响应各自声明各自的——请求头 Accept 是「期望收到的格式」,两者经常同时出现在一个 POST 里且含义不同。
- 因为 Authorization 头在传输层是明文可见的,所以凭证必须走 HTTPS——明文 HTTP 上等于把 Token 写在明信片上(HTTPS 课展开)。
- 因为缓存与条件请求全靠头驱动,所以改了 JSON 却不更新 ETag/Cache-Control,客户端会继续用旧副本——「发了版用户看不到」多半是头的锅(缓存课展开)。
- 因为中间代理会追加/改写头,所以排错要分清「浏览器发出的」与「应用收到的」——对照 Nginx 访问日志与 Spring 日志,
X-Forwarded-*就是两边传话的信使。 - 因为 HTTP/2 把头名规范为小写,所以代码里不要用大小写敏感的方式取头——用框架的大小写不敏感 API(如 Spring 的
@RequestHeader),别手写字符串精确匹配。
你可能会懵的地方
-
Accept 和 Content-Type 总搞混? 记一句话:Content-Type 说的是「我带的」,Accept 说的是「我想收的」。GET 通常只有 Accept;POST JSON 常两个都有——Content-Type 声明请求体,Accept 声明期望的响应格式。
-
为什么 Copy as cURL 那么长? 浏览器自动加了一堆安全与协商头。排查时保留关键头即可;有些头(如
sec-fetch-*)删了反而不容易复现,定位「服务器行为差异」时尽量原样保留。 -
自定义头
X-Request-Id可以加吗? 可以,是链路追踪的好实践:Nginx 或网关生成,透传到 Spring 日志。注意两点:自定义头会触发 CORS 预检(CORS 课),且头名不要带下划线以外的特殊字符——另外 Nginx 默认丢弃带下划线的头(underscores_in_headers on可改),这是真实生产坑。 -
415 与 400 怎么分? 415 是「Content-Type 我不认识/不支持」,头的问题;400 是「格式认识但内容解析/校验失败」,体的问题。联调报错先看是 415 还是 400,能直接定位改头还是改 body。
-
一个头可以出现多次吗? 可以。
Set-Cookie一条响应里可出现多次(发多张小票);Accept、Cache-Control也允许重复或逗号合并。解析时用框架的getHeaders()而不是只取第一个。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| POST JSON 不写 Content-Type | Spring 无法选择消息转换器,直接 415 | 显式 application/json;charset=UTF-8 |
| 把密钥放进自定义头却走明文 HTTP | 中间任何节点都能读走 Token | 全程 HTTPS + 短时效 Token |
出 4xx 只盯 body,不看 Allow/WWW-Authenticate/Retry-After | 错过服务器给出的修复线索 | 4xx 先读相关响应头再动手 |
| 假设代理会原样转发 Authorization | Nginx/网关可能剥离或改写,造成「本地通线上不通」 | 对照两端日志确认;内部服务间调用固定转发规则 |
| 拿到 502 里的 HTML 错误页当 JSON 解析 | 前端一堆 Unexpected token < 报错 | 先检查响应 Content-Type 与状态码,再决定怎么解析 |
卡点急救
- 本地 curl 通、线上 401:先对比两条请求的头差异——线上少没少
Authorization(网关剥离?)、Cookie 域名对不对(Domain 设成子域导致带不上)。用curl -v分别打两边,逐行 diff 请求头。 - Spring 拿到的客户端 IP 全是 127.0.0.1:流量经 Nginx 反代,应用看到的是 Nginx。确认 Nginx 有
proxy_set_header X-Forwarded-For,且 Spring 配置了信任 Forwarded 头(server.forward-headers-strategy=framework或 native)。 - 响应头里中文/文件名乱码:
Content-Disposition里的中文文件名要按 RFC 5987 编码:attachment; filename*=UTF-8''%E8%AF%BE%E8%A1%A8.xlsx;直接塞中文不同浏览器表现不一,别赌。
面试常问
-
常见的 HTTP 请求头和响应头有哪些?各什么作用? 请求头:Host(虚拟主机路由)、User-Agent(客户端标识)、Accept(期望格式)、Content-Type(请求体格式)、Authorization(凭证)、Cookie(会话)。响应头:Content-Type(响应体格式)、Content-Length(长度)、Cache-Control/ETag(缓存)、Set-Cookie(发会话票)、Location(重定向/创建资源位置)。能按「鉴权、缓存、格式、路由」四类归组比逐个背更显功底。
-
说下 X-Forwarded-For 的原理和风险? 反代把原始客户端 IP 追加进去:
client, proxy1, proxy2。风险:客户端可以伪造第一段,只有可信代理链追加的部分可靠;取「最左」可能拿到伪造 IP。正确做法是只信任自家网关追加的段,且配合X-Real-IP等由 Nginx 显式覆盖。 -
为什么有了 Content-Type 还需要 Accept? Content-Type 描述当前报文体的格式(事实),Accept 表达客户端能处理的格式(期望)。服务器据此做内容协商:同一个 URL,浏览器要 HTML、脚本要 JSON,可以返回不同表示——这是 REST「资源多表示」的基础。
自测三问
- 各举两个请求头与两个响应头,说明方向与作用。
- 管理端 POST 为什么同时需要
Content-Type与Authorization?缺一个分别会发生什么? - 在 DevTools 里如何确认某响应允许缓存?重点看哪个头?Nginx 反代后排查 IP 问题要先看什么头?
小结一句话
头是 HTTP 的便签:类型、鉴权、缓存、主机路由都写在上面——会读头,等于会读半条链路。
延伸
- MDN:HTTP headers
- MDN:Content-Type
- 下一课:HTTPS 与 TLS——明文 HTTP 上头和 body 全是明信片;TLS 如何防窃听、防篡改、防冒充,证书为什么装在 Nginx 上。