4. 在 HTTP/3 中表达 HTTP 语义 (Expressing HTTP Semantics in HTTP/3)
4.1. HTTP 消息帧 (HTTP Message Framing)
客户端在请求流 (request stream) 上发送 HTTP 请求, 该流是客户端发起的双向 QUIC 流; 参见第 6.1 节。客户端必须在给定流上仅发送单个请求。服务器在与请求相同的流上发送零个或多个中间 HTTP 响应, 然后是单个最终 HTTP 响应, 详见下文。有关中间和最终 HTTP 响应的描述, 请参见 [HTTP] 的第 15 节。
推送的响应在服务器发起的单向 QUIC 流上发送; 参见第 6.2.2 节。服务器以与标准响应相同的方式发送零个或多个中间 HTTP 响应, 然后是单个最终 HTTP 响应。推送在第 4.6 节中有更详细的描述。
在给定流上, 接收到多个请求或在最终 HTTP 响应之后接收到额外的 HTTP 响应必须被视为格式错误 (malformed)。
HTTP 消息 (请求或响应) 由以下部分组成:
-
头部段 (header section), 包括消息控制数据, 作为单个 HEADERS 帧发送,
-
可选地, 内容 (content), 如果存在, 作为一系列 DATA 帧发送, 以及
-
可选地, 尾部段 (trailer section), 如果存在, 作为单个 HEADERS 帧发送。
头部和尾部段在 [HTTP] 的第 6.3 节和第 6.5 节中描述; 内容在 [HTTP] 的第 6.4 节中描述。
接收到无效的帧序列必须被视为类型为 H3_FRAME_UNEXPECTED 的连接错误。特别是, 在任何 HEADERS 帧之前的 DATA 帧, 或尾部 HEADERS 帧之后的 HEADERS 或 DATA 帧, 被视为无效。其他帧类型, 特别是未知的帧类型, 可能会受到它们自己的规则约束; 参见第 9 节。
服务器可以在响应消息的帧之前, 之后或与之交错发送一个或多个 PUSH_PROMISE 帧。这些 PUSH_PROMISE 帧不是响应的一部分; 有关更多详细信息, 请参见第 4.6 节。推送流上不允许 PUSH_PROMISE 帧; 包含 PUSH_PROMISE 帧的推送响应必须被视为类型为 H3_FRAME_UNEXPECTED 的连接错误。
未知类型的帧 (第 9 节), 包括保留帧 (第 7.2.8 节), 可以在本节描述的其他帧之前, 之后或与之交错发送到请求或推送流上。
HEADERS 和 PUSH_PROMISE 帧可能引用对 QPACK 动态表的更新。虽然这些更新不直接是消息交换的一部分, 但它们必须在消息被消费之前接收和处理。有关更多详细信息, 请参见第 4.2 节。
HTTP/3 不定义传输编码 (transfer codings, 参见 [HTTP/1.1] 的第 7 节); Transfer-Encoding 头字段禁止使用。
当且仅当一个或多个中间响应 (1xx; 参见 [HTTP] 的第 15.2 节) 在对同一请求的最终响应之前时, 响应可以由多个消息组成。中间响应不包含内容或尾部段。
HTTP 请求/响应交换完全消耗一个客户端发起的双向 QUIC 流。发送请求后, 客户端必须关闭流以进行发送。除非使用 CONNECT 方法 (参见第 4.4 节), 否则客户端禁止使流关闭依赖于接收对其请求的响应。发送最终响应后, 服务器必须关闭流以进行发送。此时, QUIC 流完全关闭。
当流关闭时, 这表示最终 HTTP 消息的结束。由于某些消息很大或无界, 端点应当在收到足够的消息以取得进展后立即开始处理部分 HTTP 消息。如果客户端发起的流在没有足够的 HTTP 消息来提供完整响应的情况下终止, 服务器应当使用错误码 H3_REQUEST_INCOMPLETE 中止其响应流。
如果响应不依赖于尚未发送和接收的请求的任何部分, 服务器可以在客户端发送整个请求之前发送完整响应。当服务器不需要接收请求的其余部分时, 它可以中止读取请求流, 发送完整响应, 并干净地关闭流的发送部分。在请求客户端停止在请求流上发送时, 应当使用错误码 H3_NO_ERROR。客户端禁止因其请求被突然终止而丢弃完整响应, 尽管客户端出于其他原因始终可以自行决定丢弃响应。如果服务器发送部分或完整响应但不中止读取请求, 客户端应当继续发送请求内容并正常关闭流。
4.1.1. 请求取消和拒绝 (Request Cancellation and Rejection)
一旦打开了请求流, 任一端点都可以取消请求。如果响应不再引起兴趣, 客户端取消请求; 如果服务器无法或选择不响应, 服务器取消请求。在可能的情况下, 推荐服务器发送带有适当状态码的 HTTP 响应, 而不是取消已开始处理的请求。
实现应当通过突然终止流中仍然打开的任何方向来取消请求。为此, 实现重置流的发送部分并中止读取流的接收部分; 参见 [QUIC-TRANSPORT] 的第 2.4 节。
当服务器在不执行任何应用处理的情况下取消请求时, 该请求被视为 "拒绝 (rejected)"。服务器应当使用错误码 H3_REQUEST_REJECTED 中止其响应流。在此上下文中, "处理" 意味着流中的某些数据被传递到某些可能因此采取某些操作的更高层软件。客户端可以将服务器拒绝的请求视为从未发送过, 从而允许稍后重试。
服务器禁止对部分或完全处理的请求使用 H3_REQUEST_REJECTED 错误码。当服务器在部分处理后放弃响应时, 它应当使用错误码 H3_REQUEST_CANCELLED 中止其响应流。
客户端应当使用错误码 H3_REQUEST_CANCELLED 取消请求。在收到此错误码后, 如果没有执行处理, 服务器可以使用错误码 H3_REQUEST_REJECTED 突然终止响应。客户端禁止使用 H3_REQUEST_REJECTED 错误码, 除非服务器已请求使用此错误码关闭请求流。
如果流在接收完整响应后被取消, 客户端可以忽略取消并使用响应。但是, 如果流在接收部分响应后被取消, 则不应当使用该响应。只有幂等操作 (idempotent actions) 如 GET, PUT 或 DELETE 可以安全重试; 客户端不应当自动重试具有非幂等方法的请求, 除非它有某种方式知道请求语义独立于方法是幂等的, 或有某种方式检测原始请求从未应用。有关更多详细信息, 请参见 [HTTP] 的第 9.2.2 节。
4.1.2. 格式错误的请求和响应 (Malformed Requests and Responses)
格式错误的请求或响应是一个原本有效的帧序列, 但由于以下原因而无效:
-
存在禁止的字段或伪头字段,
-
缺少强制伪头字段,
-
伪头字段的值无效,
-
字段后出现伪头字段,
-
HTTP 消息的序列无效,
-
包含大写字段名称, 或
-
字段名称或值中包含无效字符。
当包含 Content-Length 头字段 ([HTTP] 的第 8.6 节) 时定义为具有内容的请求或响应, 如果 Content-Length 头字段的值不等于接收到的 DATA 帧长度之和, 则格式错误。定义为永远没有内容的响应, 即使存在 Content-Length, 也可以具有非零 Content-Length 头字段, 即使 DATA 帧中不包含内容。
处理 HTTP 请求或响应的中间方 (即, 任何不作为隧道的中间方) 禁止转发格式错误的请求或响应。检测到的格式错误的请求或响应必须被视为类型为 H3_MESSAGE_ERROR 的流错误。
对于格式错误的请求, 服务器可以在关闭或重置流之前发送指示错误的 HTTP 响应。客户端禁止接受格式错误的响应。请注意, 这些要求旨在防止针对 HTTP 的几种类型的常见攻击; 它们是故意严格的, 因为宽容可能会将实现暴露于这些漏洞。
4.2. HTTP 字段 (HTTP Fields)
HTTP 消息将元数据作为一系列键值对 (称为 "HTTP 字段") 携带; 参见 [HTTP] 的第 6.3 节和第 6.5 节。有关已注册的 HTTP 字段列表, 请参见在 https://www.iana.org/assignments/http-fields/ 维护的 "超文本传输协议 (HTTP) 字段名称注册表"。与 HTTP/2 一样, HTTP/3 对字段名称中字符的使用, Connection 头字段和伪头字段有额外的考虑。
字段名称是包含 ASCII 字符子集的字符串。[HTTP] 的第 5.1 节更详细地讨论了 HTTP 字段名称和值的属性。字段名称中的字符必须在编码之前转换为小写。包含字段名称中大写字符的请求或响应必须被视为格式错误。
HTTP/3 不使用 Connection 头字段来指示特定于连接的字段; 在此协议中, 特定于连接的元数据由其他方式传送。端点禁止生成包含特定于连接的字段的 HTTP/3 字段段; 任何包含特定于连接的字段的消息必须被视为格式错误。
唯一的例外是 TE 头字段, 它可以出现在 HTTP/3 请求头中; 当它出现时, 它禁止包含除 "trailers" 之外的任何值。
将 HTTP/1.x 消息转换为 HTTP/3 的中间方必须删除特定于连接的头字段, 如 [HTTP] 的第 7.6.1 节中所讨论的, 否则其消息将被其他 HTTP/3 端点视为格式错误。
4.2.1. 字段压缩 (Field Compression)
[QPACK] 描述了 HPACK 的一个变体, 它使编码器能够控制压缩可能导致多少队头阻塞 (head-of-line blocking)。这允许编码器平衡压缩效率与延迟。HTTP/3 使用 QPACK 压缩头部和尾部段, 包括头部段中存在的控制数据。
为了实现更好的压缩效率, Cookie 头字段 ([COOKIES]) 可以在压缩之前拆分为单独的字段行, 每个字段行包含一个或多个 cookie 对 (cookie-pairs)。如果解压缩的字段段包含多个 cookie 字段行, 则在传递到 HTTP/2 或 HTTP/3 之外的上下文 (例如 HTTP/1.1 连接或通用 HTTP 服务器应用) 之前, 必须使用两字节分隔符 "; " (ASCII 0x3b, 0x20) 将它们连接成单个字节串。
4.2.2. 头部大小约束 (Header Size Constraints)
HTTP/3 实现可以对它将在单个 HTTP 消息上接受的消息头的最大大小施加限制。接收到大于其愿意处理的头部段的服务器可以发送 HTTP 431 (Request Header Fields Too Large) 状态码 ([RFC6585])。客户端可以丢弃无法处理的响应。字段列表的大小是根据字段的未压缩大小计算的, 包括名称和值的长度 (以字节为单位), 每个字段增加 32 字节的开销。
如果实现希望将此限制通知其对等方, 它可以作为 SETTINGS_MAX_FIELD_SECTION_SIZE 参数中的字节数传送。收到此参数的实现不应当发送超过指示大小的 HTTP 消息头, 因为对等方可能会拒绝处理它。但是, HTTP 消息可以在到达源服务器之前经过一个或多个中间方; 参见 [HTTP] 的第 3.7 节。由于此限制由处理消息的每个实现单独应用, 因此不能保证低于此限制的消息会被接受。
4.3. HTTP 控制数据 (HTTP Control Data)
与 HTTP/2 一样, HTTP/3 使用一系列伪头字段, 其中字段名称以 : 字符 (ASCII 0x3a) 开头。这些伪头字段传送消息控制数据; 参见 [HTTP] 的第 6.2 节。
伪头字段不是 HTTP 字段。端点禁止生成本文档中定义以外的伪头字段。但是, 扩展可以协商修改此限制; 参见第 9 节。
伪头字段仅在定义它们的上下文中有效。为请求定义的伪头字段禁止出现在响应中; 为响应定义的伪头字段禁止出现在请求中。伪头字段禁止出现在尾部段中。端点必须将包含未定义或无效伪头字段的请求或响应视为格式错误。
所有伪头字段必须出现在头部段中常规头字段之前。任何在常规头字段之后的头部段中包含伪头字段的请求或响应必须被视为格式错误。
4.3.1. 请求伪头字段 (Request Pseudo-Header Fields)
为请求定义了以下伪头字段:
":method": 包含 HTTP 方法 ([HTTP] 的第 9 节)
":scheme": 包含目标 URI 的方案部分 ([URI] 的第 3.1 节)。
:scheme 伪头字段不限于方案为 "http" 和 "https" 的 URI。代理或网关可以转换非 HTTP 方案的请求, 从而能够使用 HTTP 与非 HTTP 服务交互。
有关使用 "https" 以外的方案的指导, 请参见第 3.1.2 节。
":authority": 包含目标 URI 的权威部分 ([URI] 的第 3.2 节)。对于方案为 "http" 或 "https" 的 URI, 权威禁止包含已弃用的 userinfo 子组件。
为确保可以准确地再现 HTTP/1.1 请求行, 当从具有方法特定形式的请求目标的 HTTP/1.1 请求进行转换时, 必须省略此伪头字段; 参见 [HTTP] 的第 7.1 节。直接生成 HTTP/3 请求的客户端应当使用 :authority 伪头字段而不是 Host 头字段。将 HTTP/3 请求转换为 HTTP/1.1 的中间方必须通过复制 :authority 伪头字段的值来创建 Host 字段 (如果请求中不存在 Host 字段)。
":path": 包含目标 URI 的路径和查询部分 ("path-absolute" 产生式以及可选的 ? 字符 (ASCII 0x3f) 后跟 "query" 产生式; 参见 [URI] 的第 3.3 节和第 3.4 节。
对于 "http" 或 "https" URI, 此伪头字段禁止为空; 不包含路径组件的 "http" 或 "https" URI 必须包含值 / (ASCII 0x2f)。不包含路径组件的 OPTIONS 请求对 :path 伪头字段包含值 * (ASCII 0x2a); 参见 [HTTP] 的第 7.1 节。
所有 HTTP/3 请求必须恰好包含 :method, :scheme 和 :path 伪头字段的一个值, 除非请求是 CONNECT 请求; 参见第 4.4 节。
如果 :scheme 伪头字段标识具有强制权威组件的方案 (包括 "http" 和 "https"), 则请求必须包含 :authority 伪头字段或 Host 头字段。如果这些字段存在, 它们禁止为空。如果两个字段都存在, 它们必须包含相同的值。如果方案没有强制权威组件并且在请求目标中未提供, 则请求禁止包含 :authority 伪头字段或 Host 头字段。
省略强制伪头字段或包含这些伪头字段的无效值的 HTTP 请求格式错误。
HTTP/3 未定义携带 HTTP/1.1 请求行中包含的版本标识符的方式。HTTP/3 请求隐式具有协议版本 "3.0"。
4.3.2. 响应伪头字段 (Response Pseudo-Header Fields)
对于响应, 定义了单个 ":status" 伪头字段, 它携带 HTTP 状态码; 参见 [HTTP] 的第 15 节。此伪头字段必须包含在所有响应中; 否则, 响应格式错误 (参见第 4.1.2 节)。
HTTP/3 未定义携带 HTTP/1.1 状态行中包含的版本或原因短语的方式。HTTP/3 响应隐式具有协议版本 "3.0"。
4.4. CONNECT 方法 (The CONNECT Method)
CONNECT 方法请求接收方建立到由请求目标标识的目标源服务器的隧道; 参见 [HTTP] 的第 9.3.6 节。它主要与 HTTP 代理一起使用, 以便与源服务器建立 TLS 会话, 以便与 "https" 资源交互。
在 HTTP/1.x 中, CONNECT 用于将整个 HTTP 连接转换为到远程主机的隧道。在 HTTP/2 和 HTTP/3 中, CONNECT 方法用于在单个流上建立隧道。
CONNECT 请求必须按如下方式构造:
-
:method 伪头字段设置为 "CONNECT"
-
:scheme 和 :path 伪头字段被省略
-
:authority 伪头字段包含要连接的主机和端口 (等效于 CONNECT 请求的请求目标的 authority-form; 参见 [HTTP] 的第 7.1 节)。
请求流在请求结束时保持打开状态以携带要传输的数据。不符合这些限制的 CONNECT 请求格式错误。
支持 CONNECT 的代理与在 :authority 伪头字段中标识的服务器建立 TCP 连接 ([RFC0793])。一旦成功建立此连接, 代理就会向客户端发送包含 2xx 系列状态码的 HEADERS 帧, 如 [HTTP] 的第 15.3 节中所定义。
流上的所有 DATA 帧对应于在 TCP 连接上发送或接收的数据。客户端发送的任何 DATA 帧的有效负载由代理传输到 TCP 服务器; 从 TCP 服务器接收的数据由代理打包成 DATA 帧。请注意, 不能保证 TCP 段的大小和数量可预测地映射到 HTTP DATA 或 QUIC STREAM 帧的大小和数量。
一旦 CONNECT 方法完成, 流上仅允许发送 DATA 帧。如果扩展定义明确允许, 可以使用扩展帧。接收到任何其他已知帧类型必须被视为类型为 H3_FRAME_UNEXPECTED 的连接错误。
TCP 连接可以由任一对等方关闭。当客户端结束请求流时 (即, 代理处的接收流进入 "Data Recvd" 状态), 代理将在其到 TCP 服务器的连接上设置 FIN 位。当代理接收到设置了 FIN 位的数据包时, 它将关闭它发送给客户端的发送流。在单个方向上保持半关闭的 TCP 连接不是无效的, 但通常被服务器处理得很差, 因此客户端不应当在仍期望从 CONNECT 目标接收数据时关闭流以进行发送。
TCP 连接错误通过突然终止流来发出信号。代理将 TCP 连接中的任何错误 (包括接收设置了 RST 位的 TCP 段) 视为类型为 H3_CONNECT_ERROR 的流错误。
相应地, 如果代理检测到流或 QUIC 连接有错误, 它必须关闭 TCP 连接。如果代理检测到客户端已重置流或中止从流读取, 它必须关闭 TCP 连接。如果流被客户端重置或中止读取, 代理应当在另一个方向上执行相同的操作, 以确保流的两个方向都被取消。在所有这些情况下, 如果底层 TCP 实现允许, 代理应当发送设置了 RST 位的 TCP 段。
由于 CONNECT 创建到任意服务器的隧道, 支持 CONNECT 的代理应当将其使用限制在一组已知端口或安全请求目标列表中; 有关更多详细信息, 请参见 [HTTP] 的第 9.3.6 节。
4.5. HTTP 升级 (HTTP Upgrade)
HTTP/3 不支持 HTTP 升级机制 ([HTTP] 的第 7.8 节) 或 101 (Switching Protocols) 信息状态码 ([HTTP] 的第 15.2.2 节)。
4.6. 服务器推送 (Server Push)
服务器推送是一种交互模式, 允许服务器在客户端发出请求之前主动向客户端推送请求-响应交换。客户端可以通过在 SETTINGS 帧中将 SETTINGS_ENABLE_PUSH 设置为 0 来禁用服务器推送。服务器禁止向已将 SETTINGS_ENABLE_PUSH 设置为 0 的客户端发送推送; 违反此规定的服务器行为必须被视为类型为 H3_SETTINGS_ERROR 的连接错误。
与 HTTP/2 一样, 服务器通过在客户端发起的请求流上发送 PUSH_PROMISE 帧 (第 7.2.5 节) 来发起推送。推送 ID 用于标识服务器推送 (参见第 4.6.1 节)。推送 ID 在 PUSH_PROMISE 帧中携带, 该帧还包括归属于服务器生成的请求的请求头段, 如 [HTTP] 的第 15 节中所述。
服务器从它发起的推送流 (第 6.2.2 节) 发送推送的响应。推送响应的传递与对常规请求的响应相同。推送响应的响应头段在 HEADERS 帧中携带, 如第 7.2.4 节所述。服务器可以通过在推送流上使用推送 ID 发送 CANCEL_PUSH 帧来取消承诺的推送。
客户端使用 MAX_PUSH_ID 帧 (第 7.2.7 节) 控制服务器可以承诺的推送数量。服务器禁止发送推送 ID 大于客户端为连接提供的最大推送 ID 的 PUSH_PROMISE 帧或 CANCEL_PUSH 帧。客户端必须将尝试这样做视为类型为 H3_ID_ERROR 的连接错误。
一旦推送流被 PUSH_PROMISE 帧打开或保留, 只要客户端未取消推送, 就可以使用推送流。一旦客户端从控制流接收到 CANCEL_PUSH 帧或从推送流接收到流终止, 推送就被取消。如果推送流在没有 CANCEL_PUSH 的情况下终止, 推送仍被视为成功完成。
客户端可以通过发送 CANCEL_PUSH 帧来中止推送。在服务器收到它之后, 如果推送尚未完成, 服务器必须中止发送推送。客户端还可以通过重置推送流来中止推送。在这两种情况下, 接收方可以安全地丢弃已接收的任何推送响应状态。
一旦请求流关闭, 实现可以选择仅缓冲对推送响应的引用或完全删除对推送响应的引用。如果在关联的请求流关闭的情况下接收到推送响应, 这并不表示推送失败。
推送流始终由推送 ID 引用。PUSH_PROMISE 帧的接收方将推送 ID 与客户端发起的流相关联, 而在推送流上接收 HEADERS 帧的客户端将推送 ID 与接收到的推送相匹配。
4.6.1. 推送 ID (Push IDs)
推送 ID 是 62 位无符号整数 (参见 [QUIC-TRANSPORT] 的第 16 节), 用于标识服务器推送。推送 ID 在连接的生命周期内是唯一的。
推送 ID 空间从零开始, 是整数空间的子集; 因此, 推送 ID 不能出现在需要流 ID 或请求 ID 的上下文中。特别是, 推送 ID 不允许出现在 GOAWAY 帧中 (参见第 5.2 节)。
推送 ID 在单个 PUSH_PROMISE 帧 (参见第 7.2.5 节) 和单个推送流 (参见第 4.6 节和第 6.2.2 节) 中使用。这些使用必须引用服务器在连接生命周期内做出的相同承诺推送。
在推送流上发送推送响应后, 推送 ID 无法重用。如果客户端从不同流接收到同一推送 ID 上的另一个推送流头或另一个 PUSH_PROMISE, 则必须将其视为类型为 H3_ID_ERROR 的连接错误。