CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
请求与响应头
这一课你将能做到
解释 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.cnAccept 请求 我希望收到什么格式 application/jsonContent-Type 双向 本侧 body 是什么格式application/json;charset=UTF-8Authorization 请求 我的凭证 Bearer eyJhbGci…Cookie 请求 浏览器自动带的小票 satoken=abc123Set-Cookie 响应 请浏览器存这张小票 见 Cookie 课 Cache-Control 响应为主 能不能缓存、缓存多久 max-age=60ETag 响应 内容指纹(版本号) "a1b2c3"If-None-Match 请求 我本地是这个版本,变了没
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 的便签:类型、鉴权、缓存、主机路由都写在上面——会读头,等于会读半条链路。
延伸