7. HTTP 消息路由
HTTP 请求消息路由由每个客户端根据目标资源, 客户端代理配置, 以及入站连接的建立或复用来确定. 对应响应路由沿同一连接链返回客户端.
7.1. 确定目标资源
虽然 HTTP 被用于各种应用, 但大多数客户端依赖与通用 Web 浏览器相同的资源标识机制和配置技术. 即使通信选项被硬编码在客户端配置中, 我们也可以把它们的组合效果视为 URI 引用 (Section 4.1).
URI 引用会被解析为绝对形式以获得 "目标 URI".目标 URI 排除引用中的 fragment 组件 (如果有), 因为 fragment identifier 保留给客户端侧处理 ([URI], Section 3.5).
为了对 "目标资源" 执行动作, 客户端发送请求消息, 其中包含已解析目标 URI 的足够组件, 使接收方能够标识同一资源. 出于历史原因, 已解析目标 URI 组件统称为 "请求目标" (request target), 它们在消息控制数据和 Host 头字段 (Section 7.2) 中发送.
有两种异常情况, 其请求目标组件采用方法特定形式:
-
对于 CONNECT (Section 9.3.6), 请求目标是隧道目的地的主机名和端口号, 二者以冒号分隔.
-
对于 OPTIONS (Section 9.3.7), 请求目标可以是单个星号 ("*").
详情见相应方法定义. 这些形式不得与其他方法一起使用.
收到客户端请求后, 服务器根据其本地配置和传入连接上下文, 从收到的组件重构目标 URI. 这种重构对于每个主要协议版本都是特定的. 例如, [HTTP/1.1] Section 3.3 定义了服务器如何确定 HTTP/1.1 请求的目标 URI.
Note: 先前规范把重组后的目标 URI 定义为一个不同概念, 即 "effective request URI".
7.2. Host 和 :authority
请求中的 "Host" 头字段提供来自目标 URI 的 host 和 port 信息, 使源服务器在为多个主机名的请求提供服务时能够区分资源.
在 HTTP/2 [HTTP/2] 和 HTTP/3 [HTTP/3] 中, 在某些情况下, Host 头字段会被请求控制数据中的 ":authority" 伪头字段取代.
Host = uri-host [ ":" port ] ; Section 4
目标 URI 的 authority 信息对处理请求至关重要. 除非用户代理把该信息作为 ":authority" 伪头字段发送, 否则必须在请求中生成 Host 头字段. 发送 Host 的用户代理应把它作为请求头部区段中的第一个字段发送.
例如, 向源服务器发送针对 ````http://www.example.org/pub/WWW/\```` 的 GET 请求会以如下内容开始:
GET /pub/WWW/ HTTP/1.1
Host: www.example.org
由于 host 和 port 信息充当应用层路由机制, 它经常成为恶意软件的目标, 用于污染共享缓存或把请求重定向到非预期服务器. 如果拦截代理在未先验证被拦截连接确实以该 host 的有效 IP 地址为目标的情况下, 依赖 host 和 port 信息将请求重定向到内部服务器, 或把它用作共享缓存中的缓存键, 则尤其脆弱.
7.3. 路由入站请求
一旦确定目标 URI 及其 origin, 客户端会决定是否需要网络请求来完成期望语义, 如果需要, 则决定该请求应被导向何处.
7.3.1. 到缓存
如果客户端拥有缓存 [CACHING] 且请求可由该缓存满足, 则请求通常首先被导向缓存.
7.3.2. 到代理
如果请求不能由缓存满足, 典型客户端会检查其配置以确定是否使用代理来满足请求. 代理配置依赖实现, 但通常基于 URI 前缀匹配, 选择性 authority 匹配, 或二者兼有, 而代理本身通常由 "http" 或 "https" URI 标识.
如果 "http" 或 "https" 代理适用, 客户端通过建立 (或复用) 到该代理的连接来入站连接, 然后向其发送 HTTP 请求消息, 其中包含与客户端目标 URI 匹配的请求目标.
7.3.3. 到 Origin
如果没有适用代理, 典型客户端会调用处理例程 (特定于目标 URI 的 scheme) 来获得对所标识资源的访问. 如何完成取决于目标 URI scheme, 并由其关联规范定义.
Section 4.3.2 定义了如何通过建立 (或复用) 到所标识源服务器的入站连接, 然后向其发送包含与客户端目标 URI 匹配请求目标的 HTTP 请求消息, 来获得对 "http" 资源的访问.
Section 4.3.3 定义了如何通过建立 (或复用) 到对所标识 origin 具有权威性的源服务器的入站安全连接, 然后向其发送包含与客户端目标 URI 匹配请求目标的 HTTP 请求消息, 来获得对 "https" 资源的访问.
7.4. 拒绝误导请求
服务器收到请求并解析到足以确定其目标 URI 后, 会决定是自行处理请求, 把请求转发到另一服务器, 将客户端重定向到不同资源, 以错误响应, 还是丢弃连接. 该决定可能受请求或连接上下文中任何内容影响, 但具体关注服务器是否已被配置为处理该目标 URI 的请求, 以及连接上下文是否适合该请求.
例如, 请求可能被有意或无意误导, 使收到的 Host 头字段中的信息不同于连接的 host 或 port. 如果连接来自受信任网关, 这种不一致可能是预期的; 否则, 它可能表示有人试图绕过安全过滤器, 欺骗服务器交付非公开内容, 或污染缓存. 关于消息路由的安全考虑见 Section 17.
除非连接来自受信任网关, 否则如果目标 URI 的任何 scheme 特定要求未满足, 源服务器必须拒绝请求. 特别是, 除非针对 "https" 资源的请求是通过已由对该目标 URI origin 有效的证书安全保护的连接接收的, 如 Section 4.2.2 所定义, 否则必须拒绝该请求.
响应中的 421 (Misdirected Request) 状态码表示源服务器拒绝了请求, 因为它看起来被误导了 (Section 15.5.20).
7.5. 响应关联
一个连接可能用于多个请求/响应交换. 用于关联请求和响应消息的机制依赖版本; HTTP 的一些版本使用消息的隐式顺序, 而其他版本使用显式标识符.
所有响应, 无论状态码如何 (包括临时响应), 都可以在收到请求后随时发送, 即使请求尚未完成. 响应可以在其对应请求完成之前完成 (Section 6.1). 同样, 客户端不预期等待某个特定时长来获得响应. 客户端 (包括中介) 如果在合理时间内未收到响应, 可能会放弃请求.
客户端如果在仍在发送相关请求时收到响应, 应继续发送该请求, 除非收到明确相反指示 (见例如 [HTTP/1.1] Section 9.5 和 [HTTP/2] Section 6.4).
7.6. 消息转发
如 Section 3.7 所述, 中介可以在处理 HTTP 请求和响应时承担多种角色. 有些中介用于提升性能或可用性. 其他中介用于访问控制或过滤内容. 由于 HTTP 流具有类似管道-过滤器架构的特征, 中介在任一方向上增强 (或干扰) 该流的程度没有固有限制.
即使协议元素未被识别 (例如新方法, 状态码或字段名), 中介也预期转发消息, 因为这为下游接收方保留了可扩展性.
不充当隧道的中介必须实现 Connection 头字段, 如 Section 7.6.1 所规定, 并排除只意图用于传入连接的字段, 不转发它们.
除非能防止无限请求循环, 中介不得把消息转发给自己. 通常, 中介应识别自己的服务器名称, 包括任何别名, 本地变体或字面 IP 地址, 并直接响应该类请求.
HTTP 消息可以作为流解析, 以便增量处理或向下游转发. 然而, 发送方和接收方不能依赖部分消息的增量交付, 因为有些实现会出于网络效率, 安全检查或内容转换的考虑缓冲或延迟消息转发.
7.6.1. Connection
"Connection" 头字段允许发送方列出当前连接所需的控制选项.
Connection = #connection-option
connection-option = token
连接选项不区分大小写.
当 Connection 以外的字段用于提供当前连接的控制信息或关于当前连接的控制信息时, 发送方必须在 Connection 头字段中列出对应字段名. 注意, 某些 HTTP 版本禁止使用字段传达此类信息, 因而不允许 Connection 字段.
中介在转发消息之前必须解析收到的 Connection 头字段, 并且对该字段中的每个 connection-option, 从消息中移除与 connection-option 同名的任何头字段或尾字段, 然后移除 Connection 头字段本身 (或将其替换为中介自己的转发消息控制选项).
因此, Connection 头字段提供了一种声明式方式, 用于区分只意图给直接接收方的字段 ("hop-by-hop") 和意图给链上所有接收方的字段 ("end-to-end"), 使消息能够自描述, 并允许未来连接特定扩展被部署, 而不必担心它们会被旧中介盲目转发.
此外, 在应用字段语义之后, 中介应移除或替换已知需要在转发前移除的字段, 无论它们是否作为 connection-option 出现. 这包括但不限于:
- Proxy-Connection ([HTTP/1.1] Appendix C.2.2)
- Keep-Alive ([RFC2068] Section 19.7.1)
- TE (Section 10.1.4)
- Transfer-Encoding ([HTTP/1.1] Section 6.1)
- Upgrade (Section 7.8)
发送方不得发送对应于意图给内容所有接收方的字段的连接选项. 例如, Cache-Control 永远不适合作为连接选项 ([CACHING] Section 5.2).
连接选项并不总是对应消息中存在的字段, 因为如果没有与连接选项关联的参数, 可能不需要连接特定字段. 相反, 收到没有对应连接选项的连接特定字段, 通常表示该字段已被中介不当转发, 接收方应忽略它.
定义不对应字段的新连接选项时, 规范作者仍应保留对应字段名以避免后续冲突. 此类保留字段名会注册到 "Hypertext Transfer Protocol (HTTP) Field Name Registry" (Section 16.3.1).
7.6.2. Max-Forwards
"Max-Forwards" 头字段为 TRACE (Section 9.3.8) 和 OPTIONS (Section 9.3.7) 请求方法提供一种机制, 用于限制请求被代理转发的次数. 当客户端试图追踪看起来在链中间失败或循环的请求时, 这可能很有用.
Max-Forwards = 1*DIGIT
Max-Forwards 值是一个十进制整数, 表示此请求消息还能被转发的剩余次数.
每个收到包含 Max-Forwards 头字段的 TRACE 或 OPTIONS 请求的中介, 都必须在转发请求之前检查并更新其值. 如果收到的值为零 (0), 中介不得转发请求; 相反, 中介必须作为最终接收方响应. 如果收到的 Max-Forwards 值大于零, 中介必须在转发消息中生成更新后的 Max-Forwards 字段, 其字段值为 a) 收到值减一 (1) 或 b) 接收方支持的 Max-Forwards 最大值 二者中的较小者.
接收方可以忽略随任何其他请求方法收到的 Max-Forwards 头字段.
7.6.3. Via
"Via" 头字段指示用户代理和服务器之间 (在请求上) 或源服务器和客户端之间 (在响应上) 存在的中间协议和接收方, 类似于电子邮件中的 "Received" 头字段 ([RFC5322] Section 3.6.7). Via 可用于跟踪消息转发, 避免请求循环, 以及识别请求/响应链上发送方的协议能力.
Via = #( received-protocol RWS received-by [ RWS comment ] )
received-protocol = [ protocol-name "/" ] protocol-version
; see Section 7.8
received-by = pseudonym [ ":" port ]
pseudonym = token
Via 字段值的每个成员都代表转发过该消息的代理或网关. 每个中介追加自身关于消息如何被接收的信息, 因而最终结果按转发接收方的序列排序.
代理必须按下文所述, 在其转发的每个消息中发送适当的 Via 头字段. HTTP-to-HTTP 网关必须在每个入站请求消息中发送适当的 Via 头字段, 并且可以在转发的响应消息中发送 Via 头字段.
对于每个中介, received-protocol 指示消息的上游发送方所使用的协议和协议版本. 因此, Via 字段值记录请求/响应链所宣告的协议能力, 使这些能力对下游接收方保持可见; 如 Section 2.5 所述, 这有助于确定哪些向后不兼容特性可安全地在响应中或后续请求中使用. 为简洁起见, 当收到的协议是 HTTP 时会省略 protocol-name.
received-by 部分通常是随后转发该消息的接收方服务器或客户端的 host 和可选 port number. 然而, 如果真实 host 被认为是敏感信息, 发送方可以用 pseudonym 替换它. 如果未提供 port, 接收方可以将其解释为该 received-protocol 的默认 port (如果有).
发送方可以生成注释来标识每个接收方的软件, 类似于 User-Agent 和 Server 头字段. 然而, Via 中的注释是可选的, 接收方可以在转发消息前移除它们.
例如, 请求消息可以由 HTTP/1.0 用户代理发送到代号为 "fred" 的内部代理, 该代理使用 HTTP/1.1 将请求转发到 p.example.net 上的公共代理, 后者通过将请求转发到 www.example.com 上的源服务器来完成该请求. 随后 www.example.com 收到的请求会具有以下 Via 头字段:
Via: 1.0 fred, 1.1 p.example.net
用作穿过网络防火墙门户的中介, 不应转发防火墙区域内主机的名称和端口, 除非已明确启用此行为. 如果未启用, 此类中介应把防火墙后任意主机的每个 received-by host 替换为该主机的适当 pseudonym.
如果条目具有相同 received-protocol 值, 中介可以把 Via 头字段列表成员的有序子序列合并为单个成员. 例如,
Via: 1.0 ricky, 1.1 ethel, 1.1 fred, 1.0 lucy
可以折叠为
Via: 1.0 ricky, 1.1 mertz, 1.0 lucy
除非多个列表成员都处于同一组织控制下, 且 host 已经被 pseudonym 替换, 发送方不应合并多个列表成员. 发送方不得合并具有不同 received-protocol 值的成员.
7.7. 消息转换
有些中介包含转换消息及其内容的功能. 例如, 代理可能在图像格式之间转换, 以节省缓存空间或减少慢速链路上的流量. 然而, 当这些转换应用于关键应用所需内容时, 可能发生操作问题, 例如医学影像或科学数据分析, 尤其是在使用完整性检查或数字签名来确保收到内容与原始内容相同的情况下.
如果 HTTP-to-HTTP 代理被设计或配置为以语义上有意义的方式修改消息, 则称为 "转换代理" (transforming proxy), 即超出正常 HTTP 处理所要求修改之外的修改, 会以对原始发送方重要或对下游接收方潜在重要的方式改变消息. 例如, 转换代理可能充当共享注释服务器 (修改响应以包含对本地注释数据库的引用), 恶意软件过滤器, 格式转码器或隐私过滤器. 此类转换被推定为选择该代理的客户端 (或客户端组织) 所期望.
如果代理收到带有非完全限定域名 host name 的目标 URI, 则它在转发请求时可以把自己的域添加到收到的 host name. 若目标 URI 包含完全限定域名, 代理不得更改 host name.
代理在把收到的目标 URI 转发到下一个入站服务器时, 不得修改其 "absolute-path" 和 "query" 部分, 除非该转发协议要求这样做. 例如, 代理经 HTTP/1.1 向源服务器转发请求时, 会根据请求方法把空路径替换为 "/" ([HTTP/1.1] Section 3.2.1) 或 "*" ([HTTP/1.1] Section 3.2.4).
代理不得转换包含 no-transform 缓存指令 ([CACHING] Section 5.2.2.6) 的响应消息内容 (Section 6.4). 注意, 这不适用于不改变内容的消息转换, 例如添加或移除 transfer codings ([HTTP/1.1] Section 7).
代理可以转换不包含 no-transform 缓存指令的消息内容. 转换 200 (OK) 响应内容的代理, 可以通过把响应状态码改为 203 (Non-Authoritative Information) (Section 15.3.4) 来通知下游接收方已应用转换.
除非字段定义明确允许修改, 或者出于隐私或安全需要认为必须修改, 代理不应修改提供关于通信链端点, 资源状态或选定表示 (内容除外) 信息的头字段.
7.8. Upgrade
"Upgrade" 头字段旨在提供一种简单机制, 用于在同一连接上从 HTTP/1.1 过渡到其他某个协议.
客户端可以在请求的 Upgrade 头字段中发送协议名列表, 按偏好降序邀请服务器在发送最终响应前切换到一个或多个命名协议. 如果服务器希望在该连接上继续使用当前协议, 可以忽略收到的 Upgrade 头字段. Upgrade 不能用于坚持要求协议变更.
Upgrade = #protocol
protocol = protocol-name ["/" protocol-version]
protocol-name = token
protocol-version = token
虽然协议名注册时带有首选大小写, 接收方在把每个 protocol-name 与支持的协议匹配时应使用不区分大小写的比较.
发送 101 (Switching Protocols) 响应的服务器必须发送 Upgrade 头字段, 以指示连接正在切换到的新协议; 如果切换多个协议层, 发送方必须按层级升序列出这些协议. 服务器不得切换到客户端未在对应请求的 Upgrade 头字段中指示的协议. 服务器可以选择忽略客户端指示的偏好顺序, 并基于其他因素选择新协议, 例如请求性质或服务器当前负载.
发送 426 (Upgrade Required) 响应的服务器必须发送 Upgrade 头字段, 以按偏好降序指示可接受协议.
服务器可以在任何其他响应中发送 Upgrade 头字段, 用于宣告其在未来请求适用时支持按偏好降序升级到所列协议.
以下是客户端发送的一个假设示例:
GET /hello HTTP/1.1
Host: www.example.com
Connection: upgrade
Upgrade: websocket, IRC/6.9, RTA/x11
协议变更后应用层通信的能力和性质完全取决于所选择的新协议. 然而, 在发送 101 (Switching Protocols) 响应后, 服务器预期立即继续响应原始请求, 就像它已在新协议中收到其等价请求一样 (即协议改变后服务器仍有一个未完成请求需要满足, 并预期在不要求重复请求的情况下完成).
例如, 如果在 GET 请求中收到 Upgrade 头字段且服务器决定切换协议, 它首先以 HTTP/1.1 响应 101 (Switching Protocols) 消息, 随后立即跟随新协议中对目标资源 GET 的等价响应. 这允许连接升级到与 HTTP 具有相同语义的协议, 而无需额外往返的延迟成本. 除非收到的消息语义能由新协议遵守, 否则服务器不得切换协议; OPTIONS 请求可以由任何协议遵守.
以下是对上述假设请求的示例响应:
HTTP/1.1 101 Switching Protocols
Connection: upgrade
Upgrade: websocket
[... data stream switches to websocket with an appropriate response
(as defined by new protocol) to the "GET /hello" request ...]
Upgrade 的发送方还必须在 Connection 头字段 (Section 7.6.1) 中发送 "Upgrade" 连接选项, 以通知中介不要转发此字段. 服务器在 HTTP/1.0 请求中收到 Upgrade 头字段时必须忽略该 Upgrade 字段.
客户端在完全发送请求消息之前, 不能开始在该连接上使用已升级协议 (即客户端不能在消息中途改变其正在发送的协议). 如果服务器同时收到 Upgrade 和带有 "100-continue" 期望 (Section 10.1.1) 的 Expect 头字段, 则服务器必须在发送 101 (Switching Protocols) 响应之前发送 100 (Continue) 响应.
Upgrade 头字段只适用于在现有连接之上切换协议; 它不能用于切换底层连接 (传输) 协议, 也不能用于把现有通信切换到不同连接. 对于这些目的, 使用 3xx (Redirection) 响应 (Section 15.4) 更合适.
本规范仅定义协议名 "HTTP", 供 Hypertext Transfer Protocol 系列使用, 如 Section 2.5 的 HTTP 版本规则以及本规范未来更新所定义. 其他协议名应使用 Section 16.7 中定义的注册程序注册.