My aggregated monorepo of OCaml code, automaintained
1# HTTP/2 Implementation Plan for ocaml-requests
2
3## Overview
4
5This document outlines the plan for adding native HTTP/2 support to the ocaml-requests library, implementing RFC 9113 (HTTP/2), RFC 7541 (HPACK header compression), and integrating seamlessly with the existing Eio-based architecture.
6
7### Design Goals
8
91. **Eio-Native**: Full integration with Eio's structured concurrency primitives
102. **Zero-Copy Where Possible**: Use `Cstruct` and `Bigstringaf` for efficient buffer management
113. **Protocol Transparency**: Users should be able to use the same API for HTTP/1.1 and HTTP/2
124. **Connection Multiplexing**: True stream multiplexing within a single TCP connection
135. **Shared Types**: Maximize type sharing between HTTP/1.1 and HTTP/2 implementations
146. **Backwards Compatibility Not Required**: This is a fresh implementation
15
16## License Attribution
17
18When deriving code or patterns from the [ocaml-h2](https://github.com/anmonteiro/ocaml-h2) library, files MUST include the following license header:
19
20```ocaml
21(*---------------------------------------------------------------------------
22 Copyright (c) 2019 António Nuno Monteiro.
23 Portions Copyright (c) 2017 Inhabited Type LLC.
24 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>.
25
26 All rights reserved.
27
28 Redistribution and use in source and binary forms, with or without
29 modification, are permitted provided that the following conditions are met:
30
31 1. Redistributions of source code must retain the above copyright notice,
32 this list of conditions and the following disclaimer.
33
34 2. Redistributions in binary form must reproduce the above copyright notice,
35 this list of conditions and the following disclaimer in the documentation
36 and/or other materials provided with the distribution.
37
38 3. Neither the name of the copyright holder nor the names of its contributors
39 may be used to endorse or promote products derived from this software
40 without specific prior written permission.
41
42 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
43 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
44 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
45 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
46 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
47 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
48 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
49 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
50 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
51 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
52 POSSIBILITY OF SUCH DAMAGE.
53 SPDX-License-Identifier: BSD-3-Clause
54 ---------------------------------------------------------------------------*)
55```
56
57Files NOT derived from h2 continue to use the ISC license header.
58
59## Specification References
60
61| RFC | Title | Status |
62|-----|-------|--------|
63| [RFC 9113](spec/rfc9113.txt) | HTTP/2 | Primary spec (obsoletes 7540) |
64| [RFC 7541](spec/rfc7541.txt) | HPACK: Header Compression for HTTP/2 | Required |
65| [RFC 9110](spec/rfc9110.txt) | HTTP Semantics | Shared with HTTP/1.1 |
66
67## Shared Types (HTTP/1.1 and HTTP/2)
68
69The requests library already provides protocol-agnostic types that will be reused for HTTP/2:
70
71| Module | Type | Description | Shared? |
72|--------|------|-------------|---------|
73| `Method` | `Method.t` | HTTP methods (GET, POST, etc.) | Yes - RFC 9110 |
74| `Status` | `Status.t` | HTTP status codes | Yes - RFC 9110 |
75| `Headers` | `Headers.t` | Header collection | Yes - with H2 pseudo-header support |
76| `Body` | `Body.t` | Request body construction | Yes |
77| `Response` | `Response.t` | Response representation | Yes |
78| `Uri` | `Uri.t` | URI parsing and manipulation | Yes - RFC 3986 |
79| `Mime` | `Mime.t` | MIME types | Yes |
80| `Error` | `Error.error` | Error types | Extended for H2 |
81| `Timeout` | `Timeout.t` | Timeout configuration | Yes |
82| `Retry` | `Retry.config` | Retry configuration | Yes |
83| `Auth` | `Auth.t` | Authentication | Yes |
84
85### HTTP/2-Specific Types (New)
86
87| Module | Type | Description |
88|--------|------|-------------|
89| `H2_frame` | `frame_type`, `frame` | Frame definitions per RFC 9113 §6 |
90| `H2_stream` | `stream_id`, `state` | Stream state machine per RFC 9113 §5.1 |
91| `H2_settings` | `settings`, `setting` | Connection settings per RFC 9113 §6.5 |
92| `H2_hpack` | `Encoder.t`, `Decoder.t` | HPACK compression per RFC 7541 |
93| `H2_error` | `error_code` | H2 error codes per RFC 9113 §7 |
94
95### Headers Module Extension
96
97The existing `Headers` module needs extension for HTTP/2 pseudo-headers:
98
99```ocaml
100(** HTTP/2 pseudo-headers per RFC 9113 §8.3 *)
101
102val get_pseudo : string -> t -> string option
103(** [get_pseudo name headers] retrieves a pseudo-header (without the colon prefix).
104 Example: [get_pseudo "method" headers] for [:method] *)
105
106val set_pseudo : string -> string -> t -> t
107(** [set_pseudo name value headers] sets a pseudo-header.
108 Pseudo-headers are placed before regular headers per RFC 9113 §8.3. *)
109
110val has_pseudo_headers : t -> bool
111(** [has_pseudo_headers headers] returns true if any pseudo-headers are present. *)
112
113val validate_h2_request : t -> (unit, string) result
114(** Validate headers for HTTP/2 request constraints per RFC 9113 §8.2.1 *)
115
116val validate_h2_response : t -> (unit, string) result
117(** Validate headers for HTTP/2 response constraints per RFC 9113 §8.2.2 *)
118```
119
120### Error Module Extension
121
122Extend `Error.error` with HTTP/2-specific variants:
123
124```ocaml
125(* Add to Error.error type *)
126
127(* HTTP/2 protocol errors *)
128| H2_protocol_error of { code: int; message: string }
129 (** HTTP/2 connection error per RFC 9113 §5.4.1 *)
130| H2_stream_error of { stream_id: int32; code: int; message: string }
131 (** HTTP/2 stream error per RFC 9113 §5.4.2 *)
132| H2_flow_control_error of { stream_id: int32 option }
133 (** Flow control window exceeded per RFC 9113 §5.2 *)
134| H2_compression_error of { message: string }
135 (** HPACK decompression failed per RFC 7541 *)
136| H2_settings_timeout
137 (** SETTINGS acknowledgment timeout per RFC 9113 §6.5.3 *)
138| H2_goaway of { last_stream_id: int32; code: int; debug: string }
139 (** Server sent GOAWAY per RFC 9113 §6.8 *)
140```
141
142## Architecture Overview
143
144### Module Structure
145
146```
147lib/
148├── # ══════════════════════════════════════════════════════════════
149├── # SHARED TYPES (Protocol-Agnostic, RFC 9110)
150├── # ══════════════════════════════════════════════════════════════
151├── method.ml[i] # HTTP methods (existing)
152├── status.ml[i] # Status codes (existing)
153├── headers.ml[i] # Headers - EXTENDED for H2 pseudo-headers
154├── body.ml[i] # Request body (existing)
155├── response.ml[i] # Response type (existing)
156├── uri.ml[i] # URI parsing (existing)
157├── error.ml[i] # Errors - EXTENDED for H2 errors
158├── auth.ml[i] # Authentication (existing)
159├── timeout.ml[i] # Timeout config (existing)
160├── retry.ml[i] # Retry config (existing)
161│
162├── # ══════════════════════════════════════════════════════════════
163├── # HTTP/1.1 IMPLEMENTATION (Existing)
164├── # ══════════════════════════════════════════════════════════════
165├── http_read.ml[i] # HTTP/1.1 response parsing
166├── http_write.ml[i] # HTTP/1.1 request serialization
167├── http_client.ml[i] # HTTP/1.1 client
168│
169├── # ══════════════════════════════════════════════════════════════
170├── # HTTP/2 IMPLEMENTATION (New - BSD-3-Clause where h2-derived)
171├── # ══════════════════════════════════════════════════════════════
172├── h2/
173│ ├── h2_frame.ml[i] # Frame types and serialization (RFC 9113 §4, §6)
174│ ├── h2_hpack.ml[i] # HPACK encoder/decoder (RFC 7541) - BSD-3-Clause
175│ ├── h2_hpack_static.ml # Static table data (RFC 7541 Appendix A)
176│ ├── h2_huffman.ml[i] # Huffman coding (RFC 7541 Appendix B) - BSD-3-Clause
177│ ├── h2_stream.ml[i] # Stream state machine (RFC 9113 §5.1) - BSD-3-Clause
178│ ├── h2_flow_control.ml[i] # Flow control windows (RFC 9113 §5.2)
179│ ├── h2_settings.ml[i] # Settings negotiation (RFC 9113 §6.5)
180│ ├── h2_connection.ml[i] # Connection lifecycle management
181│ └── h2_client.ml[i] # Client-side HTTP/2 implementation
182│
183├── # ══════════════════════════════════════════════════════════════
184├── # PROTOCOL ABSTRACTION LAYER (New)
185├── # ══════════════════════════════════════════════════════════════
186├── http_version.ml[i] # Version enum and ALPN identifiers
187├── connection.ml[i] # Unified HTTP/1.1 + HTTP/2 connection
188├── protocol.ml[i] # Protocol selection and negotiation
189│
190├── # ══════════════════════════════════════════════════════════════
191├── # PUBLIC API (Existing - Updated)
192├── # ══════════════════════════════════════════════════════════════
193├── requests.ml[i] # Session API (protocol-transparent)
194└── one.ml[i] # One-shot API (protocol-transparent)
195```
196
197### File License Summary
198
199| File Pattern | License | Reason |
200|--------------|---------|--------|
201| `h2_hpack.ml[i]` | BSD-3-Clause | Derived from h2 HPACK implementation |
202| `h2_huffman.ml[i]` | BSD-3-Clause | Derived from h2 Huffman tables |
203| `h2_stream.ml[i]` | BSD-3-Clause | State machine patterns from h2 |
204| All other files | ISC | Original implementation |
205
206### Data Flow
207
208```
209┌─────────────────────────────────────────────────────────────────┐
210│ Requests API │
211│ (get, post, put, delete - unchanged interface) │
212└─────────────────────────────┬───────────────────────────────────┘
213 │
214 ▼
215┌─────────────────────────────────────────────────────────────────┐
216│ Connection Router │
217│ - ALPN negotiation for protocol selection │
218│ - Protocol-specific handler dispatch │
219└──────────────┬──────────────────────────────────┬───────────────┘
220 │ │
221 ▼ ▼
222┌──────────────────────────┐ ┌──────────────────────────────┐
223│ HTTP/1.1 Handler │ │ HTTP/2 Handler │
224│ (existing http_client) │ │ (new h2_client) │
225└──────────────────────────┘ └──────────────┬───────────────┘
226 │
227 ┌───────────────────────┼───────────────────────┐
228 │ │ │
229 ▼ ▼ ▼
230 ┌────────────┐ ┌────────────┐ ┌────────────┐
231 │ Stream 1 │ │ Stream 3 │ │ Stream 5 │
232 │ Request A │ │ Request B │ │ Request C │
233 └────────────┘ └────────────┘ └────────────┘
234```
235
236## Phase 1: Core Frame Layer
237
238### h2_frame.ml - Frame Parsing and Serialization
239
240Implements RFC 9113 Section 4 (HTTP Frames) and Section 6 (Frame Definitions).
241
242```ocaml
243(** RFC 9113 Frame Types *)
244
245(** Frame header - 9 octets fixed per RFC 9113 §4.1 *)
246type frame_header = {
247 length : int; (** 24-bit payload length *)
248 frame_type : frame_type; (** 8-bit type *)
249 flags : flags; (** 8-bit flags *)
250 stream_id : stream_id; (** 31-bit stream identifier *)
251}
252
253(** Frame types per RFC 9113 §6 *)
254type frame_type =
255 | Data (** 0x00 - RFC 9113 §6.1 *)
256 | Headers (** 0x01 - RFC 9113 §6.2 *)
257 | Priority (** 0x02 - RFC 9113 §6.3 (deprecated but must parse) *)
258 | Rst_stream (** 0x03 - RFC 9113 §6.4 *)
259 | Settings (** 0x04 - RFC 9113 §6.5 *)
260 | Push_promise (** 0x05 - RFC 9113 §6.6 *)
261 | Ping (** 0x06 - RFC 9113 §6.7 *)
262 | Goaway (** 0x07 - RFC 9113 §6.8 *)
263 | Window_update (** 0x08 - RFC 9113 §6.9 *)
264 | Continuation (** 0x09 - RFC 9113 §6.10 *)
265 | Unknown of int
266
267(** Frame payload variants *)
268type frame_payload =
269 | Data_payload of {
270 data : Cstruct.t;
271 padding : int option;
272 end_stream : bool;
273 }
274 | Headers_payload of {
275 header_block : Cstruct.t;
276 priority : priority option;
277 end_stream : bool;
278 end_headers : bool;
279 }
280 | Settings_payload of setting list
281 | Window_update_payload of int32
282 | Rst_stream_payload of error_code
283 | Ping_payload of Cstruct.t (** 8 bytes exactly *)
284 | Goaway_payload of {
285 last_stream_id : stream_id;
286 error_code : error_code;
287 debug_data : Cstruct.t;
288 }
289 | Continuation_payload of {
290 header_block : Cstruct.t;
291 end_headers : bool;
292 }
293 | Push_promise_payload of {
294 promised_stream_id : stream_id;
295 header_block : Cstruct.t;
296 end_headers : bool;
297 }
298
299type frame = {
300 header : frame_header;
301 payload : frame_payload;
302}
303
304(** Eio-native frame reading *)
305val read_frame :
306 Eio.Buf_read.t ->
307 max_frame_size:int ->
308 (frame, error_code * string) result
309
310(** Eio-native frame writing *)
311val write_frame :
312 (Eio.Buf_write.t -> unit) ->
313 frame ->
314 unit
315```
316
317**Key Implementation Notes:**
318
3191. Use `Cstruct` for zero-copy buffer management
3202. Frame size validation per SETTINGS_MAX_FRAME_SIZE
3213. Reserved bit handling (must ignore on receive, set to 0 on send)
3224. Unknown frame type handling (MUST ignore per §5.5)
323
324### h2_error.ml - Error Codes
325
326Implements RFC 9113 Section 7 (Error Codes).
327
328```ocaml
329type error_code =
330 | No_error (** 0x00 - Graceful shutdown *)
331 | Protocol_error (** 0x01 - Protocol error detected *)
332 | Internal_error (** 0x02 - Implementation fault *)
333 | Flow_control_error (** 0x03 - Flow control violated *)
334 | Settings_timeout (** 0x04 - Settings not acknowledged *)
335 | Stream_closed (** 0x05 - Frame on closed stream *)
336 | Frame_size_error (** 0x06 - Frame size incorrect *)
337 | Refused_stream (** 0x07 - Stream not processed *)
338 | Cancel (** 0x08 - Stream cancelled *)
339 | Compression_error (** 0x09 - Compression state error *)
340 | Connect_error (** 0x0a - TCP connection error for CONNECT *)
341 | Enhance_your_calm (** 0x0b - Processing capacity exceeded *)
342 | Inadequate_security (** 0x0c - Negotiated TLS insufficient *)
343 | Http_1_1_required (** 0x0d - Use HTTP/1.1 *)
344 | Unknown of int32
345
346type error =
347 | Connection_error of error_code * string
348 | Stream_error of stream_id * error_code
349```
350
351## Phase 2: HPACK Header Compression
352
353### h2_hpack.ml - Header Compression
354
355Implements RFC 7541 (HPACK).
356
357```ocaml
358(** HPACK encoding context *)
359module Encoder : sig
360 type t
361
362 val create : capacity:int -> t
363 val set_capacity : t -> int -> unit
364
365 (** Encode headers to a buffer *)
366 val encode :
367 t ->
368 Eio.Buf_write.t ->
369 (string * string) list ->
370 unit
371end
372
373(** HPACK decoding context *)
374module Decoder : sig
375 type t
376
377 val create : capacity:int -> t
378 val set_capacity : t -> int -> unit
379
380 (** Decode a header block fragment *)
381 val decode :
382 t ->
383 Cstruct.t ->
384 ((string * string) list, error_code * string) result
385end
386
387(** Static table (RFC 7541 Appendix A) - 61 entries *)
388val static_table : (string * string) array
389
390(** Huffman encoding/decoding (RFC 7541 Appendix B) *)
391module Huffman : sig
392 val encode : string -> Cstruct.t
393 val decode : Cstruct.t -> (string, string) result
394end
395```
396
397**Key Implementation Notes:**
398
3991. Dynamic table uses FIFO eviction
4002. Static table lookup must be O(1) - use hash table
4013. Never-indexed literals for sensitive headers (cookies, auth)
4024. Integer encoding with variable-length prefix per §5.1
403
404## Phase 3: Stream State Machine
405
406### h2_stream.ml - Stream Management
407
408Implements RFC 9113 Section 5.1 (Stream States).
409
410```ocaml
411(** Stream states per RFC 9113 §5.1 Figure 2 *)
412type state =
413 | Idle
414 | Reserved_local
415 | Reserved_remote
416 | Open
417 | Half_closed_local
418 | Half_closed_remote
419 | Closed of closed_reason
420
421type closed_reason =
422 | Finished (** Normal completion with END_STREAM *)
423 | Reset_by_us of error_code
424 | Reset_by_peer of error_code
425
426(** Stream identifier - odd for client-initiated, even for server *)
427type stream_id = int32
428
429(** A single HTTP/2 stream *)
430type t = {
431 id : stream_id;
432 mutable state : state;
433 mutable send_window : int32;
434 mutable recv_window : int32;
435
436 (** Request data *)
437 request : Request.t option;
438 request_body : Body.Writer.t option;
439
440 (** Response handling *)
441 mutable response : Response.t option;
442 response_body : Eio.Stream.t; (** Backpressure-aware body stream *)
443
444 (** Completion signaling *)
445 promise : (Response.t, error) result Eio.Promise.t;
446 resolver : (Response.t, error) result Eio.Promise.u;
447}
448
449(** State transition validation *)
450val transition : t -> event -> (unit, error_code) result
451
452type event =
453 | Send_headers of { end_stream : bool }
454 | Recv_headers of { end_stream : bool }
455 | Send_data of { end_stream : bool }
456 | Recv_data of { end_stream : bool }
457 | Send_rst_stream
458 | Recv_rst_stream
459 | Send_push_promise
460 | Recv_push_promise
461
462(** Stream identifier allocation *)
463val next_stream_id : t -> stream_id
464```
465
466**Key Implementation Notes:**
467
4681. Client streams use odd IDs starting at 1
4692. Server-pushed streams use even IDs
4703. Stream IDs cannot be reused
4714. Maximum concurrent streams governed by SETTINGS
472
473## Phase 4: Flow Control
474
475### h2_flow_control.ml - Flow Control Windows
476
477Implements RFC 9113 Section 5.2 (Flow Control).
478
479```ocaml
480(** Flow control window *)
481type window = {
482 mutable size : int32;
483 mutable pending_updates : int32;
484}
485
486(** Initial window size: 65535 bytes per RFC 9113 §6.9.2 *)
487val default_window_size : int32
488
489(** Maximum window size: 2^31 - 1 *)
490val max_window_size : int32
491
492(** Connection-level flow control *)
493type connection_flow = {
494 send_window : window;
495 recv_window : window;
496}
497
498(** Stream-level flow control *)
499type stream_flow = {
500 send_window : window;
501 recv_window : window;
502}
503
504(** Consume bytes from send window, blocking if insufficient *)
505val consume_send :
506 connection_flow ->
507 stream_flow ->
508 int ->
509 unit
510
511(** Process received WINDOW_UPDATE *)
512val apply_window_update :
513 window ->
514 int32 ->
515 (unit, error_code) result
516
517(** Generate WINDOW_UPDATE when bytes consumed *)
518val update_recv_window :
519 window ->
520 int ->
521 int32 option (** Returns increment to send, if any *)
522```
523
524**Key Implementation Notes:**
525
5261. DATA frames are the only flow-controlled frames
5272. Both connection and stream level windows must have space
5283. WINDOW_UPDATE overflow is FLOW_CONTROL_ERROR
5294. Zero window size pauses sending (no busy waiting with Eio)
530
531## Phase 5: Settings Negotiation
532
533### h2_settings.ml - Connection Settings
534
535Implements RFC 9113 Section 6.5 (SETTINGS).
536
537```ocaml
538(** Settings parameters per RFC 9113 §6.5.2 *)
539type setting =
540 | Header_table_size of int (** 0x01 - Default: 4096 *)
541 | Enable_push of bool (** 0x02 - Default: true *)
542 | Max_concurrent_streams of int32 (** 0x03 - Default: unlimited *)
543 | Initial_window_size of int32 (** 0x04 - Default: 65535 *)
544 | Max_frame_size of int (** 0x05 - Default: 16384 *)
545 | Max_header_list_size of int (** 0x06 - Default: unlimited *)
546
547type t = {
548 header_table_size : int;
549 enable_push : bool;
550 max_concurrent_streams : int32 option;
551 initial_window_size : int32;
552 max_frame_size : int;
553 max_header_list_size : int option;
554}
555
556val default : t
557
558(** Validate setting values per RFC 9113 §6.5.2 *)
559val validate : setting -> (unit, error_code * string) result
560
561(** Apply settings to connection state *)
562val apply : t -> setting list -> t
563```
564
565**Key Implementation Notes:**
566
5671. SETTINGS must be acknowledged within reasonable time
5682. Initial SETTINGS in connection preface cannot be empty
5693. ENABLE_PUSH=0 from client means server MUST NOT push
5704. MAX_FRAME_SIZE range: 16384 to 16777215
571
572## Phase 6: Connection Management
573
574### h2_connection.ml - Multiplexed Connection
575
576```ocaml
577(** HTTP/2 connection state *)
578type t = {
579 flow : Eio.Flow.two_way_ty Eio.Resource.t;
580 hpack_encoder : Hpack.Encoder.t;
581 hpack_decoder : Hpack.Decoder.t;
582 mutable local_settings : Settings.t;
583 mutable remote_settings : Settings.t;
584 connection_flow : Flow_control.connection_flow;
585 streams : (stream_id, Stream.t) Hashtbl.t;
586 mutable next_stream_id : stream_id;
587 mutable goaway_received : bool;
588 mutable goaway_sent : bool;
589
590 (** Eio synchronization *)
591 write_mutex : Eio.Mutex.t;
592 pending_writes : Frame.frame Eio.Stream.t;
593}
594
595(** Connection preface - client sends magic + SETTINGS *)
596val connection_preface : string
597(** "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" *)
598
599(** Establish HTTP/2 connection *)
600val create :
601 sw:Eio.Switch.t ->
602 flow:Eio.Flow.two_way_ty Eio.Resource.t ->
603 settings:Settings.t ->
604 t
605
606(** Run the connection (spawns reader/writer fibers) *)
607val run : t -> unit
608
609(** Initiate a new stream for a request *)
610val open_stream :
611 t ->
612 Request.t ->
613 (Stream.t, error) result
614
615(** Graceful shutdown *)
616val shutdown : t -> error_code -> unit
617```
618
619### Connection Lifecycle
620
621```
622Client Server
623 | |
624 |-- Connection Preface (magic string) -------->|
625 |-- SETTINGS frame -------------------------->|
626 | |
627 |<-- SETTINGS frame --------------------------|
628 |-- SETTINGS ACK ---------------------------->|
629 |<-- SETTINGS ACK ----------------------------|
630 | |
631 |== Connection Established ===================|
632 | |
633 |-- HEADERS (stream 1) ---------------------->|
634 |<-- HEADERS (stream 1) ----------------------|
635 |-- DATA (stream 1, END_STREAM) ------------->|
636 |<-- DATA (stream 1, END_STREAM) -------------|
637 | |
638 |-- GOAWAY ---------------------------------->|
639 |<-- GOAWAY ---------------------------------|
640 | |
641```
642
643## Phase 7: Client Implementation
644
645### h2_client.ml - HTTP/2 Client
646
647This module uses shared types throughout, ensuring HTTP/2 responses are
648indistinguishable from HTTP/1.1 responses at the API level.
649
650```ocaml
651(** Make an HTTP/2 request over an existing connection.
652 Uses shared types: Method.t, Uri.t, Headers.t, Body.t → Response.t *)
653val request :
654 sw:Eio.Switch.t ->
655 connection:H2_connection.t ->
656 meth:Method.t -> (* Shared: method.ml *)
657 uri:Uri.t -> (* Shared: uri.ml *)
658 headers:Headers.t -> (* Shared: headers.ml *)
659 body:Body.t -> (* Shared: body.ml *)
660 Response.t Eio.Promise.t (* Shared: response.ml *)
661
662(** HTTP/2 pseudo-headers for requests per RFC 9113 §8.3.1
663 These are extracted from the shared types, not exposed to users *)
664type request_pseudo_headers = {
665 method_ : string; (** :method - from Method.to_string *)
666 scheme : string; (** :scheme - from Uri.scheme *)
667 authority : string; (** :authority - from Uri.host_with_port *)
668 path : string; (** :path - from Uri.path_and_query *)
669}
670
671(** Extract pseudo-headers from shared request types *)
672val pseudo_headers_of_request :
673 meth:Method.t ->
674 uri:Uri.t ->
675 request_pseudo_headers
676
677(** Encode request headers for HEADERS frame.
678 Combines pseudo-headers with regular Headers.t *)
679val encode_request_headers :
680 Hpack.Encoder.t ->
681 pseudo:request_pseudo_headers ->
682 headers:Headers.t -> (* Shared: headers.ml *)
683 Cstruct.t
684
685(** Build Response.t from HTTP/2 response headers and body stream.
686 This is where HTTP/2 frames are converted to the shared Response type. *)
687val response_of_h2 :
688 sw:Eio.Switch.t ->
689 status:int -> (* From :status pseudo-header *)
690 headers:Headers.t -> (* Shared: headers.ml, pseudo-headers stripped *)
691 body:Eio.Flow.source_ty Eio.Resource.t -> (* DATA frames as Eio flow *)
692 url:string -> (* Original request URL *)
693 elapsed:float -> (* Request timing *)
694 Response.t (* Shared: response.ml *)
695(** The returned Response.t is identical in structure to HTTP/1.1 responses.
696 Users cannot distinguish between protocols without explicit inspection. *)
697
698(** Handle PUSH_PROMISE - RFC 9113 §8.4
699 Server push is disabled by default for clients (SETTINGS_ENABLE_PUSH=0) *)
700val handle_push_promise :
701 connection:H2_connection.t ->
702 promised_stream_id:H2_stream.stream_id ->
703 headers:Headers.t ->
704 unit
705```
706
707### Response Construction Flow
708
709```
710HTTP/2 HEADERS Frame HTTP/1.1 Status Line + Headers
711 │ │
712 ▼ ▼
713┌──────────────────┐ ┌──────────────────┐
714│ Decode HPACK │ │ Parse headers │
715│ Extract :status │ │ Parse status │
716│ Strip pseudos │ │ │
717└────────┬─────────┘ └────────┬─────────┘
718 │ │
719 │ ┌────────────────────────┐ │
720 └───►│ Response.Private.make │◄─────────┘
721 │ ~status ~headers │
722 │ ~body ~url ~elapsed │
723 └───────────┬────────────┘
724 │
725 ▼
726 ┌───────────────┐
727 │ Response.t │ ◄── Same type for both protocols
728 │ (shared) │
729 └───────────────┘
730```
731
732## Phase 8: Protocol Selection (ALPN)
733
734### http_version.ml - Protocol Detection
735
736```ocaml
737(** Supported HTTP versions *)
738type version =
739 | Http_1_0
740 | Http_1_1
741 | Http_2
742
743(** Pretty printer *)
744val pp : Format.formatter -> version -> unit
745
746(** String conversion *)
747val to_string : version -> string
748
749(** ALPN protocol identifiers per RFC 9113 §3.1 *)
750val alpn_h2 : string (** "h2" - HTTP/2 over TLS *)
751val alpn_http_1_1 : string (** "http/1.1" *)
752
753(** Build ALPN list for TLS configuration *)
754val alpn_protocols : preferred:version list -> string list
755(** Returns ALPN identifiers in preference order.
756 Example: [alpn_protocols ~preferred:[Http_2; Http_1_1]] returns ["h2"; "http/1.1"] *)
757```
758
759### protocol.ml - Protocol Negotiation
760
761```ocaml
762(** Protocol negotiation and connection establishment *)
763
764(** Preferred protocol configuration *)
765type preference =
766 | Prefer_h2 (** Try HTTP/2 first, fall back to HTTP/1.1 *)
767 | Http2_only (** HTTP/2 only, fail if not supported *)
768 | Http1_only (** HTTP/1.1 only, no ALPN negotiation *)
769 | Auto (** Use ALPN result, default to HTTP/1.1 *)
770
771(** Establish connection with protocol negotiation *)
772val connect :
773 sw:Eio.Switch.t ->
774 net:_ Eio.Net.t ->
775 clock:_ Eio.Time.clock ->
776 tls_config:Tls.Config.client option ->
777 preference:preference ->
778 host:string ->
779 port:int ->
780 Connection.t
781(** Performs:
782 1. TCP connection
783 2. TLS handshake with ALPN (if HTTPS)
784 3. Protocol detection from ALPN result
785 4. HTTP/2 connection preface (if HTTP/2)
786 5. Returns unified Connection.t *)
787
788(** Detect protocol from established TLS connection *)
789val detect_from_alpn : Tls_eio.t -> Http_version.version option
790
791(** Check if server supports HTTP/2 (for connection reuse decisions) *)
792val supports_h2 : Connection.t -> bool
793```
794
795## Phase 9: Unified Connection Abstraction
796
797### connection.ml - Protocol-Agnostic Connection
798
799This module provides a unified interface that works with both HTTP/1.1 and HTTP/2,
800using the shared types from the requests library.
801
802```ocaml
803(** HTTP protocol version *)
804type version =
805 | Http_1_1
806 | Http_2
807
808(** A connection that can be HTTP/1.1 or HTTP/2 *)
809type t =
810 | Http1 of {
811 flow : Eio.Flow.two_way_ty Eio.Resource.t;
812 version : [ `Http_1_0 | `Http_1_1 ];
813 }
814 | Http2 of {
815 connection : H2_connection.t;
816 }
817
818(** Connection pool key - for HTTP/2, one connection handles all streams *)
819type pool_key = {
820 host : string;
821 port : int;
822 scheme : [ `Http | `Https ];
823}
824
825(** Request using shared types - protocol handled internally *)
826type request = {
827 meth : Method.t; (** Shared: method.ml *)
828 uri : Uri.t; (** Shared: uri.ml *)
829 headers : Headers.t; (** Shared: headers.ml *)
830 body : Body.t; (** Shared: body.ml *)
831}
832
833(** Execute a request on the appropriate protocol.
834 Returns Response.t (shared type) regardless of underlying protocol. *)
835val execute :
836 t ->
837 sw:Eio.Switch.t ->
838 clock:_ Eio.Time.clock ->
839 request:request ->
840 auto_decompress:bool ->
841 Response.t
842(** The response uses shared types:
843 - [Response.status] returns [Status.t] (shared)
844 - [Response.headers] returns [Headers.t] (shared)
845 - [Response.body] returns [Eio.Flow.source] (protocol-agnostic stream) *)
846
847(** Check if connection can accept more streams.
848 - HTTP/1.1: Always true (one request at a time, pipelining not supported)
849 - HTTP/2: True if under MAX_CONCURRENT_STREAMS limit *)
850val can_accept_stream : t -> bool
851
852(** Get the negotiated protocol version *)
853val version : t -> version
854
855(** HTTP/2-specific: number of active streams *)
856val active_streams : t -> int
857```
858
859### Type Flow Diagram
860
861```
862 ┌─────────────────────────────────────────┐
863 │ User Code │
864 │ Requests.get session url │
865 └───────────────────┬─────────────────────┘
866 │
867 ┌───────────────────▼─────────────────────┐
868 │ Shared Request Types │
869 │ Method.t, Uri.t, Headers.t, Body.t │
870 └───────────────────┬─────────────────────┘
871 │
872 ┌─────────────────────────┼─────────────────────────┐
873 │ │ │
874 ▼ ▼ ▼
875 ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
876 │ HTTP/1.1 │ │ Connection │ │ HTTP/2 │
877 │ http_write │◄─────│ Abstraction │─────►│ h2_client │
878 │ http_read │ │ connection.ml │ │ h2_frame │
879 └────────┬────────┘ └─────────────────┘ └────────┬────────┘
880 │ │
881 │ ┌─────────────────────────┐ │
882 └────────►│ Shared Response Type │◄─────────────┘
883 │ Response.t │
884 │ Status.t, Headers.t │
885 │ Eio.Flow.source (body) │
886 └─────────────────────────┘
887```
888
889## Phase 10: Updated Requests API
890
891### Changes to requests.ml
892
893The main API remains unchanged from the user's perspective. Internal changes:
894
8951. **Connection Pool Changes**: HTTP/2 connections are multiplexed, so pooling strategy differs
8962. **ALPN Negotiation**: Automatic protocol selection for HTTPS
8973. **Stream Multiplexing**: Multiple concurrent requests on single connection
898
899```ocaml
900(** Internal: choose protocol and execute request *)
901let make_request_internal t ~method_ ~uri ~headers ~body =
902 let scheme = Uri.scheme uri in
903 let pool = match scheme with
904 | "http" -> t.http_pool
905 | "https" -> t.https_pool
906 | _ -> raise (Invalid_argument "unsupported scheme")
907 in
908
909 Conpool.with_connection pool (Uri.host_with_port uri) @@ fun conn ->
910 match conn with
911 | Connection.Http1 _ ->
912 (* Existing HTTP/1.1 code path *)
913 Http_client.make_request ...
914 | Connection.Http2 { connection } ->
915 (* New HTTP/2 code path *)
916 let stream = H2_connection.open_stream connection request in
917 Eio.Promise.await stream.promise
918```
919
920## Implementation Phases
921
922### Phase 0: Shared Type Extensions (Week 1) - COMPLETED
923- [x] Extend `headers.ml` with pseudo-header support for HTTP/2
924- [x] Extend `error.ml` with HTTP/2 error variants
925- [x] Add `http_version.ml` - version type and ALPN identifiers
926- [x] Unit tests for header pseudo-header validation (28 tests passing)
927
928### Phase 1: Frame Layer (Week 1-2) - COMPLETED
929- [x] `h2/h2_frame.ml` - Frame types, parsing, serialization
930- [x] Unit tests for all frame types (37 tests passing)
931- [ ] Fuzz tests for frame parsing robustness
932
933### Phase 2: HPACK (Week 2-3) - BSD-3-Clause files
934- [ ] `h2/h2_hpack_static.ml` - Static table (RFC 7541 Appendix A)
935- [ ] `h2/h2_huffman.ml` - Huffman encode/decode (RFC 7541 Appendix B)
936- [ ] `h2/h2_hpack.ml` - Encoder and Decoder
937 - [ ] Integer encoding with prefix
938 - [ ] String literal encoding
939 - [ ] Dynamic table with eviction
940 - [ ] Indexed vs literal header representations
941- [ ] Unit tests with RFC 7541 Appendix C examples
942
943### Phase 3: Stream State Machine (Week 3-4) - BSD-3-Clause
944- [ ] `h2/h2_stream.ml` - Stream state machine
945 - [ ] State transitions per RFC 9113 §5.1
946 - [ ] Stream ID allocation (odd for client)
947 - [ ] Stream lifecycle (idle → open → half-closed → closed)
948- [ ] `h2/h2_flow_control.ml` - Window management
949 - [ ] Connection and stream level windows
950 - [ ] WINDOW_UPDATE generation
951 - [ ] Backpressure via Eio.Stream
952- [ ] Unit tests for state transitions and flow control
953
954### Phase 4: Connection Management (Week 4-5)
955- [ ] `h2/h2_settings.ml` - Settings frame handling
956- [ ] `h2/h2_connection.ml` - Connection lifecycle
957 - [ ] Connection preface exchange
958 - [ ] Settings negotiation
959 - [ ] GOAWAY handling
960 - [ ] PING/PONG
961 - [ ] Reader/writer fiber management
962- [ ] `h2/h2_client.ml` - Client request handling
963 - [ ] Request → HEADERS frame conversion
964 - [ ] Response handling with shared Response.t
965 - [ ] Trailer support
966- [ ] Integration tests with mock HTTP/2 server
967
968### Phase 5: Protocol Abstraction (Week 5-6)
969- [ ] `protocol.ml` - Protocol negotiation
970- [ ] `connection.ml` - Unified connection type
971- [ ] Update `requests.ml`:
972 - [ ] ALPN preference configuration
973 - [ ] Protocol-transparent request execution
974 - [ ] Connection pool adaptation for HTTP/2 multiplexing
975- [ ] Update `one.ml` for HTTP/2 support
976- [ ] Integration tests with real HTTP/2 servers
977
978### Phase 6: Testing & Documentation (Week 6-7)
979- [ ] h2spec compliance testing (all test groups)
980- [ ] Interoperability testing (nginx, Cloudflare, Google)
981- [ ] Performance benchmarks vs HTTP/1.1
982- [ ] Update module documentation with RFC references
983- [ ] Usage examples in documentation
984- [ ] CHANGELOG entry
985
986## Key Differences from ocaml-h2
987
988| Aspect | ocaml-h2 | Our Implementation |
989|--------|----------|-------------------|
990| I/O Model | Callback-based, runtime-agnostic | Native Eio with structured concurrency |
991| Parsing | Angstrom | Direct Eio.Buf_read parsing |
992| Serialization | Faraday | Eio.Buf_write |
993| Concurrency | External via Lwt/Async/Eio adapters | Built-in Eio fibers and promises |
994| Flow Control | Manual callbacks | Eio.Stream backpressure |
995| Error Handling | Result types + callbacks | Eio exceptions + Result |
996| Types | Standalone Request/Response types | Shared with HTTP/1.1 (Method, Status, Headers, Body, Response) |
997| Integration | Separate library | Built into requests library |
998
999### Shared Type Benefits
1000
10011. **Single API Surface**: Users work with the same types regardless of protocol
10022. **No Conversion Overhead**: Response from HTTP/2 uses same `Response.t` as HTTP/1.1
10033. **Consistent Error Handling**: Extended `Error.error` type covers both protocols
10044. **Unified Headers**: `Headers.t` works for both, with H2 pseudo-header extensions
10055. **Protocol Transparency**: `Requests.get` works the same for HTTP/1.1 and HTTP/2
1006
1007## OCamldoc Templates
1008
1009### Module-level reference:
1010```ocaml
1011(** RFC 9113 HTTP/2 Frame handling.
1012
1013 This module implements HTTP/2 frame parsing and serialization as specified in
1014 {{:https://datatracker.ietf.org/doc/html/rfc9113#section-4}RFC 9113 Section 4}
1015 and {{:https://datatracker.ietf.org/doc/html/rfc9113#section-6}Section 6}. *)
1016```
1017
1018### Section-specific reference:
1019```ocaml
1020(** Stream state machine per
1021 {{:https://datatracker.ietf.org/doc/html/rfc9113#section-5.1}RFC 9113 Section 5.1}. *)
1022```
1023
1024### HPACK reference:
1025```ocaml
1026(** HPACK header compression per
1027 {{:https://datatracker.ietf.org/doc/html/rfc7541}RFC 7541}. *)
1028```
1029
1030## Testing Strategy
1031
1032### Unit Tests
1033- Each module has comprehensive unit tests
1034- Use RFC examples as test vectors (especially HPACK Appendix C)
1035- Property-based tests for frame parsing/serialization roundtrips
1036
1037### Integration Tests
1038- Mock HTTP/2 server for controlled testing
1039- Full request/response cycles
1040- Error condition handling (RST_STREAM, GOAWAY)
1041- Flow control behavior under load
1042
1043### h2spec Compliance
1044Run the [h2spec](https://github.com/summerwind/h2spec) test suite:
1045```bash
1046h2spec -h localhost -p 8443 --tls --insecure
1047```
1048All test groups must pass:
1049- Generic tests
1050- HPACK tests
1051- HTTP/2 tests (all sections)
1052
1053### Interoperability Testing
1054Test against major HTTP/2 servers:
1055- nginx (most common)
1056- Cloudflare (strict implementation)
1057- Google (reference implementation)
1058- Apache (with mod_http2)
1059
1060### Shared Type Verification
1061Verify that responses from HTTP/2 are indistinguishable from HTTP/1.1:
1062```ocaml
1063(* This test should pass regardless of protocol *)
1064let test_shared_response protocol =
1065 let response = match protocol with
1066 | `Http1 -> make_http1_request ()
1067 | `Http2 -> make_http2_request ()
1068 in
1069 (* All these use shared types *)
1070 assert (Response.status response = `OK);
1071 assert (Headers.get `Content_type (Response.headers response) = Some "application/json");
1072 let body = Response.text response in
1073 assert (String.length body > 0)
1074```
1075
1076### Fuzz Testing
1077Property-based testing for:
1078- Frame parsing (malformed frames should not crash)
1079- HPACK decoding (compression bombs, invalid sequences)
1080- Header validation (injection attacks)
1081
1082## Dependencies
1083
1084- `eio` - Structured concurrency runtime (existing)
1085- `cstruct` - Zero-copy buffer management (may already exist)
1086- `tls-eio` - TLS with ALPN support (existing)
1087- No new external dependencies for core HTTP/2
1088
1089## Risks and Mitigations
1090
1091| Risk | Mitigation |
1092|------|------------|
1093| HPACK complexity | Follow RFC 7541 examples exactly, extensive testing |
1094| Flow control deadlocks | Use Eio.Stream for backpressure, careful window management |
1095| Connection multiplexing bugs | State machine validation, property-based testing |
1096| Performance regression | Benchmark early and often, profile critical paths |
1097
1098## References
1099
11001. [RFC 9113 - HTTP/2](https://datatracker.ietf.org/doc/html/rfc9113)
11012. [RFC 7541 - HPACK](https://datatracker.ietf.org/doc/html/rfc7541)
11023. [RFC 9110 - HTTP Semantics](https://datatracker.ietf.org/doc/html/rfc9110)
11034. [ocaml-h2 Reference Implementation](https://github.com/anmonteiro/ocaml-h2)
11045. [h2spec Test Suite](https://github.com/summerwind/h2spec)