HTTP/2 framing, HPACK and connection state, I/O-free
0

Configure Feed

Select the types of activity you want to include in your feed.

ocaml-h2 / lib / stream.ml
18 kB 431 lines
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2019 Antonio Nuno Monteiro. Copyright (c) 2025 Anil Madhavapeddy 3 <anil@recoil.org>. 4 5 All rights reserved. 6 7 Redistribution and use in source and binary forms, with or without 8 modification, are permitted provided that the following conditions are met: 9 10 1. Redistributions of source code must retain the above copyright notice, this 11 list of conditions and the following disclaimer. 12 13 2. Redistributions in binary form must reproduce the above copyright notice, 14 this list of conditions and the following disclaimer in the documentation 15 and/or other materials provided with the distribution. 16 17 3. Neither the name of the copyright holder nor the names of its contributors 18 may be used to endorse or promote products derived from this software without 19 specific prior written permission. 20 21 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 SPDX-License-Identifier: BSD-3-Clause 32 ---------------------------------------------------------------------------*) 33 34(** HTTP/2 Stream State Machine per RFC 9113 Section 5.1. 35 36 Implements the stream lifecycle: 37 - idle -> open -> half-closed -> closed 38 - With reserved states for server push (PUSH_PROMISE) 39 40 See 41 {{:https://datatracker.ietf.org/doc/html/rfc9113#section-5.1}RFC 9113 42 Section 5.1}. *) 43 44(* ============================================================ Stream States 45 ============================================================ *) 46 47(** Why a stream was closed. *) 48type closed_reason = 49 | Finished (** Both endpoints sent END_STREAM. *) 50 | Reset_by_us of Frame.error_code (** We sent RST_STREAM. *) 51 | Reset_by_them of Frame.error_code (** Peer sent RST_STREAM. *) 52 53(** Stream state per RFC 9113 Section 5.1. *) 54type state = 55 | Idle 56 (** Initial state. Transitions to open on send/recv HEADERS, or to 57 reserved on PUSH_PROMISE. *) 58 | Reserved_local 59 (** We sent PUSH_PROMISE. Can only send HEADERS (-> half_closed_remote) or 60 RST_STREAM (-> closed). *) 61 | Reserved_remote 62 (** Peer sent PUSH_PROMISE. Can only receive HEADERS (-> 63 half_closed_local) or send/recv RST_STREAM (-> closed). *) 64 | Open 65 (** Both sides can send frames. Transitions to half_closed on END_STREAM, 66 or closed on RST_STREAM. *) 67 | Half_closed_local 68 (** We sent END_STREAM. We can only receive frames. Transitions to closed 69 on recv END_STREAM or RST_STREAM. *) 70 | Half_closed_remote 71 (** Peer sent END_STREAM. We can only send frames. Transitions to closed 72 on send END_STREAM or RST_STREAM. *) 73 | Closed of closed_reason (** Terminal state. *) 74 75(** Events that cause state transitions. *) 76type event = 77 | Send_headers of { end_stream : bool } 78 | Recv_headers of { end_stream : bool } 79 | Send_data of { end_stream : bool } 80 | Recv_data of { end_stream : bool } 81 | Send_push_promise 82 | Recv_push_promise 83 | Send_rst_stream of Frame.error_code 84 | Recv_rst_stream of Frame.error_code 85 | Send_end_stream 86 | Recv_end_stream 87 88(* ============================================================ State Transition 89 Logic ============================================================ *) 90 91(** [transition state event] applies [event] to [state] and returns the new 92 state or an error. 93 94 Implements RFC 9113 Section 5.1 state machine. *) 95let transition state event : (state, Frame.error_code * string) result = 96 match (state, event) with 97 (* === Idle state transitions === *) 98 | Idle, Send_headers { end_stream = false } -> Ok Open 99 | Idle, Send_headers { end_stream = true } -> Ok Half_closed_local 100 | Idle, Recv_headers { end_stream = false } -> Ok Open 101 | Idle, Recv_headers { end_stream = true } -> Ok Half_closed_remote 102 | Idle, Send_push_promise -> Ok Reserved_local 103 | Idle, Recv_push_promise -> Ok Reserved_remote 104 | Idle, _ -> Error (Frame.Protocol_error, "Invalid frame on idle stream") 105 (* === Reserved (local) state transitions === *) 106 | Reserved_local, Send_headers { end_stream = false } -> Ok Half_closed_remote 107 | Reserved_local, Send_headers { end_stream = true } -> 108 (* Send HEADERS with END_STREAM on reserved -> closed *) 109 Ok (Closed Finished) 110 | Reserved_local, Send_rst_stream code -> Ok (Closed (Reset_by_us code)) 111 | Reserved_local, Recv_rst_stream code -> Ok (Closed (Reset_by_them code)) 112 | Reserved_local, _ -> 113 Error (Frame.Protocol_error, "Invalid frame on reserved (local) stream") 114 (* === Reserved (remote) state transitions === *) 115 | Reserved_remote, Recv_headers { end_stream = false } -> Ok Half_closed_local 116 | Reserved_remote, Recv_headers { end_stream = true } -> 117 (* Recv HEADERS with END_STREAM on reserved -> closed *) 118 Ok (Closed Finished) 119 | Reserved_remote, Send_rst_stream code -> Ok (Closed (Reset_by_us code)) 120 | Reserved_remote, Recv_rst_stream code -> Ok (Closed (Reset_by_them code)) 121 | Reserved_remote, _ -> 122 Error (Frame.Protocol_error, "Invalid frame on reserved (remote) stream") 123 (* === Open state transitions === *) 124 | Open, Send_headers { end_stream = true } -> Ok Half_closed_local 125 | Open, Send_headers { end_stream = false } -> 126 Ok Open (* Trailers without END_STREAM, unusual but valid *) 127 | Open, Recv_headers { end_stream = true } -> Ok Half_closed_remote 128 | Open, Recv_headers { end_stream = false } -> 129 Ok Open (* Trailers without END_STREAM *) 130 | Open, Send_data { end_stream = true } -> Ok Half_closed_local 131 | Open, Send_data { end_stream = false } -> Ok Open 132 | Open, Recv_data { end_stream = true } -> Ok Half_closed_remote 133 | Open, Recv_data { end_stream = false } -> Ok Open 134 | Open, Send_end_stream -> Ok Half_closed_local 135 | Open, Recv_end_stream -> Ok Half_closed_remote 136 | Open, Send_rst_stream code -> Ok (Closed (Reset_by_us code)) 137 | Open, Recv_rst_stream code -> Ok (Closed (Reset_by_them code)) 138 | Open, Send_push_promise | Open, Recv_push_promise -> 139 (* PUSH_PROMISE is sent on an existing stream but creates a new reserved 140 stream *) 141 Ok Open 142 (* === Half-closed (local) state transitions === *) 143 | Half_closed_local, Recv_headers { end_stream = true } -> 144 Ok (Closed Finished) 145 | Half_closed_local, Recv_headers { end_stream = false } -> 146 Ok Half_closed_local 147 | Half_closed_local, Recv_data { end_stream = true } -> Ok (Closed Finished) 148 | Half_closed_local, Recv_data { end_stream = false } -> Ok Half_closed_local 149 | Half_closed_local, Recv_end_stream -> Ok (Closed Finished) 150 | Half_closed_local, Send_rst_stream code -> Ok (Closed (Reset_by_us code)) 151 | Half_closed_local, Recv_rst_stream code -> Ok (Closed (Reset_by_them code)) 152 | Half_closed_local, (Send_headers _ | Send_data _ | Send_end_stream) -> 153 Error (Frame.Stream_closed, "Cannot send on half-closed (local) stream") 154 | Half_closed_local, _ -> 155 Ok Half_closed_local (* WINDOW_UPDATE, PRIORITY allowed *) 156 (* === Half-closed (remote) state transitions === *) 157 | Half_closed_remote, Send_headers { end_stream = true } -> 158 Ok (Closed Finished) 159 | Half_closed_remote, Send_headers { end_stream = false } -> 160 Ok Half_closed_remote 161 | Half_closed_remote, Send_data { end_stream = true } -> Ok (Closed Finished) 162 | Half_closed_remote, Send_data { end_stream = false } -> 163 Ok Half_closed_remote 164 | Half_closed_remote, Send_end_stream -> Ok (Closed Finished) 165 | Half_closed_remote, Send_rst_stream code -> Ok (Closed (Reset_by_us code)) 166 | Half_closed_remote, Recv_rst_stream code -> Ok (Closed (Reset_by_them code)) 167 | Half_closed_remote, (Recv_headers _ | Recv_data _ | Recv_end_stream) -> 168 Error (Frame.Stream_closed, "Received data on half-closed (remote) stream") 169 | Half_closed_remote, _ -> 170 Ok Half_closed_remote (* WINDOW_UPDATE, PRIORITY allowed *) 171 (* === Closed state - terminal === *) 172 | Closed reason, Recv_rst_stream _ -> 173 (* Can receive RST_STREAM on closed stream (race condition) *) 174 Ok (Closed reason) 175 | Closed _, _ -> Error (Frame.Stream_closed, "Stream is closed") 176 177(* ============================================================ Stream Type 178 ============================================================ *) 179 180type flow_control = { 181 send_window : int; (** Bytes we're allowed to send. *) 182 recv_window : int; (** Bytes we've advertised we can receive. *) 183 initial_send_window : int; (** Initial send window from SETTINGS. *) 184 initial_recv_window : int; (** Initial receive window from SETTINGS. *) 185} 186(** Flow control state for a stream. *) 187 188type t = { 189 id : int32; 190 (** Stream identifier (odd = client-initiated, even = server-initiated). 191 *) 192 state : state; (** Current stream state. *) 193 flow : flow_control; (** Flow control windows. *) 194 request_headers : Hpack.header list option; 195 (** Request headers once received/sent. *) 196 response_headers : Hpack.header list option; 197 (** Response headers once received/sent. *) 198 trailers : Hpack.header list option; (** Trailing headers. *) 199 send_queue : Rope.t; 200 (** Outbound DATA accepted from the application but not yet emitted: held 201 here until the connection and stream send windows admit it. *) 202 send_fin : bool; (** END_STREAM has been requested for the queued data. *) 203 fin_sent : bool; (** The END_STREAM DATA frame has been emitted. *) 204} 205(** A single HTTP/2 stream: an immutable value; transitions return a new [t]. *) 206 207(** Default initial flow control window size per RFC 9113. *) 208let default_initial_window_size = 65535 209 210(** Create a new stream with the given ID. *) 211let v ?(initial_send_window = default_initial_window_size) 212 ?(initial_recv_window = default_initial_window_size) id = 213 { 214 id; 215 state = Idle; 216 flow = 217 { 218 send_window = initial_send_window; 219 recv_window = initial_recv_window; 220 initial_send_window; 221 initial_recv_window; 222 }; 223 request_headers = None; 224 response_headers = None; 225 trailers = None; 226 send_queue = Rope.empty; 227 send_fin = false; 228 fin_sent = false; 229 } 230 231(** Get the stream ID. *) 232let id t = t.id 233 234(** Get the current state. *) 235let state t = t.state 236 237(** Helper for state predicate checks. *) 238let state_matches pred t = pred t.state 239 240(** Check if stream is in idle state. *) 241let is_idle = state_matches (function Idle -> true | _ -> false) 242 243(** Check if stream is open (including half-closed). *) 244let is_active = 245 state_matches (function 246 | Open | Half_closed_local | Half_closed_remote -> true 247 | _ -> false) 248 249(** Check if stream is fully open (both directions). *) 250let is_open = state_matches (function Open -> true | _ -> false) 251 252(** Check if stream is closed. *) 253let is_closed = state_matches (function Closed _ -> true | _ -> false) 254 255(** Check if we can send on this stream. *) 256let can_send = 257 state_matches (function 258 | Open | Half_closed_remote | Reserved_local -> true 259 | _ -> false) 260 261(** Check if we can receive on this stream. *) 262let can_recv = 263 state_matches (function 264 | Open | Half_closed_local | Reserved_remote -> true 265 | _ -> false) 266 267(** Apply an event to the stream. Returns [Ok t'] with the advanced stream, or 268 [Error (code, msg)] on an invalid transition. *) 269let apply_event t event = 270 match transition t.state event with 271 | Ok new_state -> Ok { t with state = new_state } 272 | Error (code, msg) -> Error (code, msg) 273 274(* ============================================================ Flow Control 275 ============================================================ *) 276 277(** Consume bytes from the send window. Returns the number of bytes actually 278 consumed (may be less if window exhausted) and the advanced stream. *) 279let consume_send_window t bytes = 280 let available = min bytes t.flow.send_window in 281 ( available, 282 { 283 t with 284 flow = { t.flow with send_window = t.flow.send_window - available }; 285 } ) 286 287(* RFC 9113 s6.9.1: flow-control windows must not exceed 2^31-1. Compared in 288 int64 so the bound is exact even where the native int is narrower than 32 289 bits (there the window can never reach it and the check never fires). *) 290let max_flow_window = 0x7FFF_FFFFL 291 292(** Add bytes to the send window (from WINDOW_UPDATE). *) 293let credit_send_window t increment = 294 let new_window = t.flow.send_window + increment in 295 if Int64.compare (Int64.of_int new_window) max_flow_window > 0 then 296 Error 297 ( Frame.Flow_control_error, 298 "WINDOW_UPDATE would overflow stream send window" ) 299 else Ok { t with flow = { t.flow with send_window = new_window } } 300 301(** Consume bytes from the receive window. Call this when receiving DATA frames. 302*) 303let consume_recv_window t bytes = 304 { t with flow = { t.flow with recv_window = t.flow.recv_window - bytes } } 305 306(** Credit the receive window (we're sending WINDOW_UPDATE). *) 307let credit_recv_window t increment = 308 { t with flow = { t.flow with recv_window = t.flow.recv_window + increment } } 309 310(** Get available send window. *) 311let send_window t = t.flow.send_window 312 313(** Get available receive window. *) 314let recv_window t = t.flow.recv_window 315 316(** Get initial receive window size. *) 317let initial_recv_window t = t.flow.initial_recv_window 318 319(** Update initial window size (from SETTINGS). Adjusts the current window by 320 the delta. *) 321let update_initial_window_size t new_initial_size = 322 let delta = new_initial_size - t.flow.initial_send_window in 323 let new_window = t.flow.send_window + delta in 324 if 325 Int64.compare (Int64.of_int new_window) max_flow_window > 0 326 || new_window < 0 327 then 328 Error 329 ( Frame.Flow_control_error, 330 "Flow control window overflow after SETTINGS update" ) 331 else Ok { t with flow = { t.flow with send_window = new_window } } 332 333(* ============================================================ Send Queue 334 ============================================================ *) 335 336(** Append [payload] to the outbound DATA queue. [end_stream] records that the 337 queued data is the last the application will send. *) 338let enqueue_send t ?(end_stream = false) payload = 339 { 340 t with 341 send_queue = Rope.concat t.send_queue payload; 342 send_fin = t.send_fin || end_stream; 343 } 344 345(** Bytes queued for sending but not yet emitted. *) 346let pending_send t = Rope.length t.send_queue 347 348(** Whether END_STREAM has been requested for the queued data. *) 349let send_fin t = t.send_fin 350 351(** Whether the END_STREAM DATA frame has already been emitted. *) 352let fin_sent t = t.fin_sent 353 354(** Record that the END_STREAM DATA frame has been emitted. *) 355let mark_fin_sent t = { t with fin_sent = true } 356 357(** Remove and return the first [n] bytes of the send queue ([n] is clamped to 358 what is queued) and the stream with that prefix dropped. *) 359let take_send t n = 360 let len = Rope.length t.send_queue in 361 let n = min n len in 362 let head = Rope.sub t.send_queue 0 n in 363 (head, { t with send_queue = Rope.sub t.send_queue n (len - n) }) 364 365(* ============================================================ Stream 366 Identifier Management 367 ============================================================ *) 368 369(** Check if stream ID is client-initiated (odd). *) 370let is_client_initiated id = Int32.(logand id 1l = 1l) 371 372(** Check if stream ID is server-initiated (even, non-zero). *) 373let is_server_initiated id = Int32.(id > 0l && logand id 1l = 0l) 374 375(** Check if stream ID is valid (non-zero). *) 376let is_valid_id id = Int32.compare id 0l > 0 377 378(** Connection-level stream ID is 0. *) 379let connection_stream_id = 0l 380 381(** Check if this is the connection-level stream. *) 382let is_connection_stream id = Int32.equal id 0l 383 384(* ============================================================ Pretty Printing 385 ============================================================ *) 386 387let pp_closed_reason fmt = function 388 | Finished -> Fmt.pf fmt "Finished" 389 | Reset_by_us code -> Fmt.pf fmt "Reset_by_us(%a)" Frame.pp_error_code code 390 | Reset_by_them code -> 391 Fmt.pf fmt "Reset_by_them(%a)" Frame.pp_error_code code 392 393let pp_state fmt = function 394 | Idle -> Fmt.pf fmt "Idle" 395 | Reserved_local -> Fmt.pf fmt "Reserved(local)" 396 | Reserved_remote -> Fmt.pf fmt "Reserved(remote)" 397 | Open -> Fmt.pf fmt "Open" 398 | Half_closed_local -> Fmt.pf fmt "HalfClosed(local)" 399 | Half_closed_remote -> Fmt.pf fmt "HalfClosed(remote)" 400 | Closed reason -> Fmt.pf fmt "Closed(%a)" pp_closed_reason reason 401 402let string_of_state state = Fmt.str "%a" pp_state state 403 404let pp fmt t = 405 Fmt.pf fmt "Stream{id=%ld; state=%a; send_window=%d; recv_window=%d}" t.id 406 pp_state t.state t.flow.send_window t.flow.recv_window 407 408(* ============================================================ Headers 409 Management ============================================================ *) 410 411(** Set request headers, returning the updated stream. *) 412let set_request_headers t headers = { t with request_headers = Some headers } 413 414(** Get request headers. *) 415let request_headers t = t.request_headers 416 417(** Set response headers, returning the updated stream. *) 418let set_response_headers t headers = { t with response_headers = Some headers } 419 420(** Get response headers. *) 421let response_headers t = t.response_headers 422 423(** Set trailers, returning the updated stream. *) 424let set_trailers t headers = { t with trailers = Some headers } 425 426(** Get trailers. *) 427let trailers t = t.trailers 428 429(** Reset the stream with an error code, returning the closed stream. *) 430let reset t code = 431 match apply_event t (Send_rst_stream code) with Ok t -> t | Error _ -> t