首部分两种寿命:端到端的一路传到最终接收者,逐跳的只对相邻两个节点有效, 代理转发时必须删掉。逐跳的有八个固定的,另有 Connection 点名的任意个。
上一节区分了字段的来源,这一节区分它们的寿命。 这条区别是写代理、调 CDN、排查「为什么某个首部到不了后端」的关键。
一、八个固定的逐跳字段
| 字段 | 为什么是逐跳 |
|---|---|
Connection |
它描述的就是本条连接,换一条就没意义 |
Keep-Alive |
连接保活参数,只对这条连接有效 |
Proxy-Authenticate |
向「下一跳」要代理认证 |
Proxy-Authorization |
给「下一跳」的代理凭据 |
TE |
告诉「下一跳」我能接受哪些传输编码 |
Trailer |
声明尾块里有什么,只对本次传输有效 |
Transfer-Encoding |
本次传输怎么切块 |
Upgrade |
请「下一跳」换协议 |
这八个之外,Connection 还能当场点名: |
|
Connection: X-Secret 的意思就是「X-Secret 这一跳用完就扔」。 |
|
| 这个机制是 RFC 7230 留的口子:让中间件可以临时约定一些只在本跳有意义的字段,不用去标准化。 | |
| 反过来说,任何被点名的字段,代理都必须删掉再转发。 |
二、为什么要删
因为这些东西一旦传到下一跳,含义就变了。
最典型的例子是 Proxy-Authorization。
它是给第一个代理的凭据,如果原样转发给源服务器,等于把你的代理密码送给了别人。
Transfer-Encoding 更危险:代理如果解了分块,却把 Transfer-Encoding: chunked 原样转发出去,
下一跳就会对着一段已经没有分块标记的字节去找块长,结果就是把正文当首部解析。
这类不一致正是请求走私的成因之一。
所以「删掉逐跳字段」不是可选的优化,是正确性要求。
三、实测:九个首部进去,三个出来
Demo 里客户端发了 9 个首部:Host、Connection: close, X-Secret、X-Secret、Keep-Alive、
TE、Upgrade、Proxy-Authorization、Accept、X-Trace-Id。
代理扔掉六个:Connection、Keep-Alive、Proxy-Authorization、TE、Upgrade、X-Secret。
注意 X-Secret 不在那八个固定清单里——它是被 Connection 点名的,所以同样被扔。
这正是「动态逐跳」的实证。
源服务器最后收到 3 个:Host、Accept、X-Trace-Id。
X-Trace-Id 是端到端的,一路带到底,这种字段通常用来做全链路追踪。
四、动手 Demo:代理按规矩删掉逐跳字段
起一个真代理,把请求转发给源服务器,转发前按 RFC 7230 删掉八个固定逐跳字段, 再加上 Connection 里点名的那些,最后对比两边的首部清单。
HOP = [u'connection', u'keep-alive', u'proxy-authenticate',
u'proxy-authorization', u'te', u'trailer',
u'transfer-encoding', u'upgrade']
drop = set(HOP)
for ln in lines:
if ln.lower().startswith(u'connection:'):
for tok in ln.split(u':', 1)[1].split(u','):
t = tok.strip().lower()
if t and t not in (u'close', u'keep-alive'):
drop.add(t) # 点名的也是逐跳
keep = []
for ln in lines[1:]:
k = ln.split(u':', 1)[0].strip()
if k.lower() in drop or k.lower() == u'host':
continue
keep.append((k, ln.split(u':', 1)[1].strip()))
实跑输出:
--------------------------------------------------------------------
逐跳首部:代理必须删掉的那几种
--------------------------------------------------------------------
客户端发给代理的:
Host
Connection
X-Secret
Keep-Alive
TE
Upgrade
Proxy-Authorization
Accept
X-Trace-Id
代理按规矩扔掉这些(逐跳,只对本跳有效):
丢掉 Connection | Keep-Alive | Proxy-Authorization | TE | Upgrade | X-Secret
源服务器实际收到的:
Host
Accept
X-Trace-Id
客户端发了几个首部 9 个
源服务器收到几个 3 个
逐跳首部只在「相邻两个节点之间」有意义,代理转发时必须自己消化掉。
八个固定的是:Connection、Keep-Alive、Proxy-Authenticate、Proxy-Authorization、TE、Trailer、Transfer-Encoding、Upgrade
还有一个动态的:Connection 里点了谁的名字,谁就是逐跳的。
所以 X-Secret 被删了——它不在八个固定清单里,是 Connection 点名的。
这正是 RFC 7230 留的口子:让中间件可以临时约定「这一跳专用的字段」。
客户端发 9 个,源服务器收到 3 个,中间少了 6 个。
被扔掉的是:Connection、Keep-Alive、Proxy-Authorization、TE、Upgrade、X-Secret。
X-Secret 这一项最值得注意——它没出现在任何标准清单里,
是 Connection: close, X-Secret 里那半句把它标成逐跳的。
活下来的是 Host、Accept、X-Trace-Id。
前两个是端到端首部,最后一个虽然是自定义字段,但没有被点名,所以一路带到底。
30 秒实验
- 把
Connection: close, X-Secret改成Connection: X-Trace-Id, 看X-Trace-Id会不会也被扔掉。 - 给代理加一个
Via: 1.1 myproxy,看它是不是应该被保留(答案是保留,它是端到端)。 - 想一想:如果一个代理忘了删
Transfer-Encoding,下一跳会怎么理解正文?
常见误区
- 「所有首部都该原样转发」——逐跳字段必须消化掉,否则语义会错。
- 「逐跳字段就是那八个」——Connection 点名的任意字段也是逐跳。
- 「删逐跳字段是为了省流量」——是为了正确性,不是优化。
自测三问(点开对答案)
1. 八个固定的逐跳字段是哪几个? —— Connection、Keep-Alive、Proxy-Authenticate、Proxy-Authorization、TE、Trailer、 Transfer-Encoding、Upgrade。 2. `Connection: X-Secret` 是什么意思? —— 把 X-Secret 标记成本跳专用的字段,代理转发时必须删掉。 3. 为什么 `Proxy-Authorization` 绝对不能转发给源服务器? —— 那是给代理的凭据,转发出去等于泄露代理账号密码。通用首部讲完了它的第一条:Connection 的逐跳机制。下一节开始逐个讲 6.3 那九个通用字段。
- ☐ (本节完,下一节 6.3.1 Cache-Control)