📘 第六章 HTTP首部
📄 一句话概括

首部分两种寿命:端到端的一路传到最终接收者,逐跳的只对相邻两个节点有效, 代理转发时必须删掉。逐跳的有八个固定的,另有 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 秒实验

  1. 把 Connection: close, X-Secret 改成 Connection: X-Trace-Id, 看 X-Trace-Id 会不会也被扔掉。
  2. 给代理加一个 Via: 1.1 myproxy,看它是不是应该被保留(答案是保留,它是端到端)。
  3. 想一想:如果一个代理忘了删 Transfer-Encoding,下一跳会怎么理解正文?

常见误区

  1. 「所有首部都该原样转发」——逐跳字段必须消化掉,否则语义会错。
  2. 「逐跳字段就是那八个」——Connection 点名的任意字段也是逐跳。
  3. 「删逐跳字段是为了省流量」——是为了正确性,不是优化。
自测三问(点开对答案) 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 那九个通用字段。

相关笔记