摘要

  • RFC 2068 把 HTTP 版本号定义为发送者对报文格式和理解后续通信能力的声明,不是当前报文所用功能的清单。
  • 代理解释并重发报文时,会成为下一跳的新发送者;它必须使用自己真正支持的版本,不能借用上游的能力标签。
  • 同一大版本内的演进依靠发送者承担兼容成本:面向旧接收方的高版本报文,删掉旧规范未定义的字段后仍须是有效旧版本报文。

假设运维人员在源站入口抓到一条请求:

GET /archive HTTP/1.1

这条记录能证明什么?它能证明直接连接源站的发送者生成了一条采用 HTTP/1.1 格式的请求,并在合规语义下声称自己具备相应能力。它不能单独证明最初的浏览器也用 HTTP/1.1 发出请求;中间任何代理都可能终止上一跳、改写字段,再建立下一跳。它也不能证明这次请求使用了分块传输、持久连接、缓存控制或其他 HTTP/1.1 功能。

版本号的价值,恰恰来自这种克制。它为接收者提供一项可以用于下一步判断的局部事实,却不替代对功能、路径和结果的分别观察。

HTTP/1.0 已经暴露“名字不等于能力”

1996 年 5 月发布的 RFC 1945 记录 HTTP/1.0 的共同使用,性质是 Informational,而不是互联网标准。正文把通常一致实现的功能与少量或实现不一致的功能分开。这个结构本身提醒读者:写着 HTTP/1.0 的程序,不等于具备完全一致的行为。

RFC 1945 的版本章节已经提出后来沿用的原则:版本政策用于表达发送者正在使用的报文格式,以及它理解后续 HTTP 通信的能力,而不是本次通信实际获得了什么功能。

RFC 2068 的引言进一步指出,当时出现了许多自称 HTTP/1.0、却只实现一部分功能的应用。若版本只是产品自报名称,通信双方无法据此判断对方下一步能理解什么。版本号必须附带可检查的规范义务,才能成为有用信号。

大版本和小版本承担不同任务。报文格式发生不兼容变化时才增加大版本;不改变总体解析算法、但增加语义或发送者能力时,可以增加小版本。仅在已经可扩展的字段里增加取值,若不改变通信行为,并不自动要求新版本。

这套安排没有追求“所有参与者同时升级”。它先固定必须共同理解的骨架,再让新增能力在不破坏旧骨架的前提下出现。

版本上限、功能使用与执行结果是三张收据

RFC 2068 要求按该规范发送请求或响应的应用,在首行写入 HTTP/1.1。使用这个版本号,表示发送应用至少对规范达到条件符合。一个应用的 HTTP 版本,是它至少达到条件符合的最高版本。

这里至少存在三种不同证据。第一种是报文语法:接收者要按什么格式解析当前消息。第二种是发送者声明的能力上限:它愿意为哪个版本的义务负责。第三种是这条消息实际使用的功能集合。

第三种通常小于第二种。一条没有正文、没有新字段的简单请求仍可合法标为 HTTP/1.1,因为版本还要告诉对方未来响应或请求可采用什么能力。反过来,看到 HTTP/1.1 也不能认定某个具体功能已经启用。

版本号同样不证明身份。一个缺陷实现可能误报,一个恶意程序也可以写下虚假字符。规范定义的是合规声明应当如何解释,不是给实现签发密码学证书。它不证明报文最终抵达、字段未被中介改动、服务器接受了内容、数据持久保存或业务操作完成。

把这些收据拆开,能避免“一个数字替代全部观测”。接收者可以把能力声明用于后续决定,却必须为每个实际功能另找证据。

代理不能继承上游的版本

HTTP 链路中的代理不是透明复印机。它可以解析请求、处理缓存、修改字段,再向下一台服务器生成新请求。RFC 2068 因而明确规定,代理或网关不得发送高于自身实际版本的版本标识。

如果代理收到比自己更高版本的请求,它有三类合规选择:降低请求版本、返回错误,或者改为隧道,不再解释所承载的 HTTP 报文。如果收到较低版本请求,它在某些情况下可以升级后再转发。版本转换可能要求增加或删除字段。

关键不是“原始报文来自谁”,而是“下一条报文由谁生成”。HTTP/1.0 代理不能因为上游客户端支持 HTTP/1.1,就在出站请求里原样复制高版本。它一旦解释并重建消息,就必须为自己的能力声明负责。

1997 年 5 月的 RFC 2145 正是为版本语义混乱而写。它把规则说得更直接:HTTP 版本号是逐跳组件,不是端到端组件;代理不会“转发”请求或响应的版本号。Via 可以另行保留各段观察到的协议与发送者信息,但它和当前首行版本是两张不同收据。

因此,源站入口的 HTTP/1.1 只能指向直接相邻的发送者。若要知道客户端到源站每一段使用什么协议,必须逐段观察,不能把末端抓包逆推为全路径事实。

未知字段不是免费兼容,而是一笔发送者预算

小版本演进要想不迫使所有旧实现立即退出,就必须保证旧接收者仍能从新消息里得到有效核心。RFC 2145 记载,早期文本的含义引发争论,不同实现因此出现互操作问题。它的澄清首先锁定了既有语义:同一大版本内,小版本不能改变旧头字段的解释。

高版本发送者仍然可以向低版本接收者发送后者不认识的新字段,但不能依赖对方理解这个字段。HTTP/1.1 报文发给 HTTP/1.0 或版本未知的接收者时,删掉 HTTP/1.0 未定义的字段以后,剩余内容必须仍是有效 HTTP/1.0 报文。

这是一个很强的“删除测试”。选择新能力的人要承担向后兼容成本,旧实现不用猜测未来。新字段可以带来额外价值,但它不能成为旧接收者无法完成解析或正确处理的隐藏前提。

未知字段还要区分端到端扩展与逐跳控制。代理通常应继续转发它不认识的字段,让更远端、可能理解该扩展的接收者有机会使用。被 Connection 指定为只对当前连接有效的字段则必须在下一跳移除。

更不能把报文 framing 所必需的语义伪装成“旧端忽略即可”的新字段。RFC 2145 用分块传输说明边界:HTTP/1.1 服务器不能针对 HTTP/1.0 请求发送 Transfer-Encoding: chunked,因为旧客户端若不理解分块,根本无法正确界定正文。

向后兼容不是宽容一切,而是保证去掉陌生部分以后,旧的那部分仍然完整成立。

一份澄清 RFC 留下了怎样的历史证据

RFC 2145 说,它不是修改 HTTP/1.0 与 HTTP/1.1 的既定意图,而是在有歧义时给出设计者意图的权威说明。它还明确记载,误解和不一致已经造成某些互操作问题。

这能证明当时存在解释与实现之间的摩擦,却不能证明受影响的厂商、产品数量、地区或流量比例。文章只能沿着文档留下的边界陈述,不应把“发生过问题”扩写成“互联网普遍故障”。

RFC 2145 还限制版本选择。客户端通常应发送自己至少条件符合、且大版本不超过已知服务端能力的最高版本;服务器应发送自己至少条件符合、且大版本不超过请求的最高版本。任何一方都不得声明自己不符合的版本。

如果已知某个对端错误处理较高版本,可以在观察到缺陷后降级。这是为具体错误实现准备的例外,而非默认做法。无证据地永远降级,会让正确实现得不到新能力,也会使版本信号逐渐失真;虚报高版本则会诱导对方调用不存在的能力。

1999 年的 RFC 2616 延续了核心语义并引用 RFC 2145。2014 年的 RFC 7230 取代这两份文档,把小版本的意义说得更加明确:即使当前报文只用了向后兼容的功能子集,小版本仍然广告发送者对未来通信的能力。RFC 7230 还记录,从 RFC 2068 到 RFC 2616 并没有增加小版本号,说明“文档修订”“规范要求变化”和“线上版本字符变化”不是同一事件。

2022 年的 RFC 9110 又把跨版本的 HTTP 语义与 HTTP/1.1、HTTP/2、HTTP/3 各自的报文语法分开。三种大版本并非后一种简单废除前一种,它们在不同情境各有属性。中介转发时,要把协议版本更新为自己实际用于下一跳的版本;Via 继续承担记录上游段能力的另一项任务。

只让共同层证明它真正能证明的事

用 Lu Heng 后来提出的“最小初始规范、局部未来决策、自愿采用”观察这段历史,可以看到一种克制的协调结构:共同层只规定互操作必需的报文骨架与兼容义务;每个运行代码的接收者依据本地实现决定如何处理;新能力要通过实现、验证和使用才成为运行事实。

这只是后见编辑透镜,不是 1997 年作者意图的历史证明,更不能把 Lu Heng 关于分布式账本和互联网治理的主张倒灌进 HTTP 工作组。它的用途,是提醒我们不要把 RFC 发布、版本字符、实现能力、功能执行和业务结果压成同一个状态。

RFC 2068 的版本号没有承诺整条链路“一切都是 1.1”。它只要求每个发送者在自己掌握的一跳上诚实发言;要求新发送者为旧接收者留下有效消息;把是否采用下一项能力的决定留给真正承担后果的对端。

来源