A batteries included HTTP/1.1 client in OCaml
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** HTTP/2 Frame Layer.
7
8 This module implements HTTP/2 frame parsing and serialization as specified in
9 {{:https://datatracker.ietf.org/doc/html/rfc9113#section-4}RFC 9113 Section 4}
10 and {{:https://datatracker.ietf.org/doc/html/rfc9113#section-6}RFC 9113 Section 6}.
11
12 {2 Frame Structure}
13
14 All HTTP/2 frames share a common 9-octet header:
15 {v
16 +-----------------------------------------------+
17 | Length (24) |
18 +---------------+---------------+---------------+
19 | Type (8) | Flags (8) |
20 +-+-------------+---------------+-------------------------------+
21 |R| Stream Identifier (31) |
22 +=+=============================================================+
23 | Frame Payload (0...) ...
24 +---------------------------------------------------------------+
25 v}
26
27 Per RFC 9113 Section 4.1:
28 - Length: 24-bit unsigned integer (payload length, not including header)
29 - Type: 8-bit frame type
30 - Flags: 8-bit flags specific to frame type
31 - R: Reserved 1-bit (MUST be 0 when sending, MUST be ignored when receiving)
32 - Stream Identifier: 31-bit unsigned integer (0 for connection-level frames)
33*)
34
35(** {1 Stream Identifier} *)
36
37type stream_id = int32
38(** Stream identifier. Per RFC 9113 Section 5.1.1:
39 - Client-initiated streams use odd numbers
40 - Server-initiated streams use even numbers
41 - Stream 0 is reserved for connection-level frames *)
42
43val stream_id_is_client_initiated : stream_id -> bool
44(** [stream_id_is_client_initiated id] returns true for odd stream IDs. *)
45
46val stream_id_is_server_initiated : stream_id -> bool
47(** [stream_id_is_server_initiated id] returns true for even non-zero stream IDs. *)
48
49(** {1 Frame Types}
50
51 Per {{:https://datatracker.ietf.org/doc/html/rfc9113#section-6}RFC 9113 Section 6}. *)
52
53type frame_type =
54 | Data (** 0x00 - RFC 9113 Section 6.1 *)
55 | Headers (** 0x01 - RFC 9113 Section 6.2 *)
56 | Priority (** 0x02 - RFC 9113 Section 6.3 (deprecated) *)
57 | Rst_stream (** 0x03 - RFC 9113 Section 6.4 *)
58 | Settings (** 0x04 - RFC 9113 Section 6.5 *)
59 | Push_promise (** 0x05 - RFC 9113 Section 6.6 *)
60 | Ping (** 0x06 - RFC 9113 Section 6.7 *)
61 | Goaway (** 0x07 - RFC 9113 Section 6.8 *)
62 | Window_update (** 0x08 - RFC 9113 Section 6.9 *)
63 | Continuation (** 0x09 - RFC 9113 Section 6.10 *)
64 | Unknown of int (** Unknown frame type - MUST be ignored per RFC 9113 Section 5.5 *)
65
66val frame_type_to_int : frame_type -> int
67(** Convert frame type to its numeric value. *)
68
69val frame_type_of_int : int -> frame_type
70(** Convert numeric value to frame type. *)
71
72val pp_frame_type : Format.formatter -> frame_type -> unit
73(** Pretty printer for frame types. *)
74
75(** {1 Frame Flags} *)
76
77module Flags : sig
78 type t = int
79 (** Frame flags as a bitmask. *)
80
81 (** {2 Common Flags} *)
82
83 val none : t
84 (** No flags set. *)
85
86 val end_stream : t
87 (** END_STREAM (0x01) - Indicates final frame in a stream. *)
88
89 val end_headers : t
90 (** END_HEADERS (0x04) - Indicates header block is complete. *)
91
92 val padded : t
93 (** PADDED (0x08) - Frame is padded. *)
94
95 val priority : t
96 (** PRIORITY (0x20) - Priority information present. *)
97
98 val ack : t
99 (** ACK (0x01) - Acknowledgment (for SETTINGS and PING). *)
100
101 (** {2 Flag Operations} *)
102
103 val test : t -> t -> bool
104 (** [test flags flag] returns true if [flag] is set in [flags]. *)
105
106 val set : t -> t -> t
107 (** [set flags flag] returns [flags] with [flag] set. *)
108
109 val clear : t -> t -> t
110 (** [clear flags flag] returns [flags] with [flag] cleared. *)
111
112 val pp : Format.formatter -> t -> unit
113 (** Pretty printer for flags. *)
114end
115
116(** {1 Frame Header} *)
117
118type frame_header = {
119 length : int;
120 (** Payload length (24-bit unsigned). MUST NOT exceed SETTINGS_MAX_FRAME_SIZE. *)
121 frame_type : frame_type;
122 (** Frame type (8-bit). *)
123 flags : Flags.t;
124 (** Frame-specific flags (8-bit). *)
125 stream_id : stream_id;
126 (** Stream identifier (31-bit). 0 for connection-level frames. *)
127}
128
129val frame_header_length : int
130(** Frame header is always 9 octets. *)
131
132val pp_frame_header : Format.formatter -> frame_header -> unit
133(** Pretty printer for frame headers. *)
134
135(** {1 Error Codes}
136
137 Per {{:https://datatracker.ietf.org/doc/html/rfc9113#section-7}RFC 9113 Section 7}. *)
138
139type error_code =
140 | No_error (** 0x00 - Graceful shutdown *)
141 | Protocol_error (** 0x01 - Protocol error detected *)
142 | Internal_error (** 0x02 - Implementation fault *)
143 | Flow_control_error (** 0x03 - Flow control limits exceeded *)
144 | Settings_timeout (** 0x04 - Settings not acknowledged in time *)
145 | Stream_closed (** 0x05 - Frame received for closed stream *)
146 | Frame_size_error (** 0x06 - Frame size incorrect *)
147 | Refused_stream (** 0x07 - Stream not processed *)
148 | Cancel (** 0x08 - Stream cancelled *)
149 | Compression_error (** 0x09 - Compression state not updated *)
150 | Connect_error (** 0x0a - TCP connection error for CONNECT *)
151 | Enhance_your_calm (** 0x0b - Processing capacity exceeded *)
152 | Inadequate_security (** 0x0c - Negotiated TLS parameters not acceptable *)
153 | Http_1_1_required (** 0x0d - Use HTTP/1.1 for this request *)
154 | Unknown_error of int32 (** Unknown error code *)
155
156val error_code_to_int32 : error_code -> int32
157(** Convert error code to its numeric value. *)
158
159val error_code_of_int32 : int32 -> error_code
160(** Convert numeric value to error code. *)
161
162val error_code_to_string : error_code -> string
163(** Convert error code to its string representation. *)
164
165val pp_error_code : Format.formatter -> error_code -> unit
166(** Pretty printer for error codes. *)
167
168(** {1 Settings}
169
170 Per {{:https://datatracker.ietf.org/doc/html/rfc9113#section-6.5.2}RFC 9113 Section 6.5.2}. *)
171
172type setting =
173 | Header_table_size of int (** 0x01 - HPACK dynamic table size *)
174 | Enable_push of bool (** 0x02 - Server push enabled *)
175 | Max_concurrent_streams of int32 (** 0x03 - Maximum concurrent streams *)
176 | Initial_window_size of int32 (** 0x04 - Initial flow control window *)
177 | Max_frame_size of int (** 0x05 - Maximum frame payload size *)
178 | Max_header_list_size of int (** 0x06 - Maximum header list size *)
179 | No_rfc7540_priorities of bool (** 0x09 - RFC 9113: Deprecate RFC 7540 priorities *)
180 | Unknown_setting of int * int32 (** Unknown setting (id, value) *)
181
182val setting_to_pair : setting -> int * int32
183(** Convert setting to (identifier, value) pair. *)
184
185val setting_of_pair : int -> int32 -> setting
186(** Convert (identifier, value) pair to setting. *)
187
188val pp_setting : Format.formatter -> setting -> unit
189(** Pretty printer for settings. *)
190
191(** {1 Priority}
192
193 Per {{:https://datatracker.ietf.org/doc/html/rfc9113#section-6.3}RFC 9113 Section 6.3}.
194
195 Note: Stream prioritization is deprecated in RFC 9113 but MUST still be parsed. *)
196
197type priority = {
198 exclusive : bool;
199 (** Exclusive dependency flag. *)
200 stream_dependency : stream_id;
201 (** Stream this one depends on. *)
202 weight : int;
203 (** Weight 1-256 (stored as 1-256, not 0-255). *)
204}
205
206val default_priority : priority
207(** Default priority: non-exclusive, depends on 0, weight 16. *)
208
209val pp_priority : Format.formatter -> priority -> unit
210(** Pretty printer for priority. *)
211
212(** {1 Frame Payloads} *)
213
214type frame_payload =
215 | Data_payload of {
216 data : Cstruct.t;
217 (** The actual data being transferred. *)
218 }
219 | Headers_payload of {
220 priority : priority option;
221 (** Priority if PRIORITY flag set. *)
222 header_block : Cstruct.t;
223 (** Encoded header block fragment (HPACK). *)
224 }
225 | Priority_payload of priority
226 (** Priority specification (deprecated). *)
227 | Rst_stream_payload of error_code
228 (** Error code for stream termination. *)
229 | Settings_payload of setting list
230 (** List of settings. Empty list for ACK. *)
231 | Push_promise_payload of {
232 promised_stream_id : stream_id;
233 (** Stream ID being reserved. *)
234 header_block : Cstruct.t;
235 (** Encoded header block fragment. *)
236 }
237 | Ping_payload of Cstruct.t
238 (** 8 bytes of opaque data. *)
239 | Goaway_payload of {
240 last_stream_id : stream_id;
241 (** Highest processed stream ID. *)
242 error_code : error_code;
243 (** Reason for closing connection. *)
244 debug_data : Cstruct.t;
245 (** Optional diagnostic data. *)
246 }
247 | Window_update_payload of int32
248 (** Window size increment (1 to 2^31-1). *)
249 | Continuation_payload of {
250 header_block : Cstruct.t;
251 (** Continuation of header block. *)
252 }
253 | Unknown_payload of Cstruct.t
254 (** Payload for unknown frame types. *)
255
256(** {1 Complete Frame} *)
257
258type frame = {
259 header : frame_header;
260 (** Frame header. *)
261 payload : frame_payload;
262 (** Frame payload. *)
263}
264
265val pp_frame : Format.formatter -> frame -> unit
266(** Pretty printer for frames. *)
267
268(** {1 Frame Parsing}
269
270 Parse frames from Eio buffered input. *)
271
272type parse_error =
273 | Incomplete
274 (** Not enough data available. *)
275 | Frame_size_error of string
276 (** Frame size exceeds limits. *)
277 | Protocol_error of string
278 (** Protocol violation. *)
279
280val pp_parse_error : Format.formatter -> parse_error -> unit
281(** Pretty printer for parse errors. *)
282
283val parse_frame_header : Cstruct.t -> (frame_header, parse_error) result
284(** [parse_frame_header buf] parses a 9-byte frame header.
285 Returns [Error Incomplete] if buffer is too small. *)
286
287val parse_frame_payload :
288 frame_header ->
289 Cstruct.t ->
290 (frame_payload, parse_error) result
291(** [parse_frame_payload header buf] parses frame payload based on type.
292 The buffer should contain exactly [header.length] bytes. *)
293
294val parse_frame :
295 Cstruct.t ->
296 max_frame_size:int ->
297 (frame * int, parse_error) result
298(** [parse_frame buf ~max_frame_size] parses a complete frame.
299 Returns the frame and number of bytes consumed.
300 [max_frame_size] is the current SETTINGS_MAX_FRAME_SIZE value.
301 Returns [Error Frame_size_error] if payload exceeds limit. *)
302
303(** {1 Frame Serialization}
304
305 Serialize frames to Eio buffered output. *)
306
307val serialize_frame_header : frame_header -> Cstruct.t
308(** [serialize_frame_header header] serializes a frame header to 9 bytes. *)
309
310val serialize_frame : frame -> Cstruct.t
311(** [serialize_frame frame] serializes a complete frame. *)
312
313val write_frame : Eio.Buf_write.t -> frame -> unit
314(** [write_frame writer frame] writes a frame to the buffer. *)
315
316(** {1 Frame Construction Helpers} *)
317
318val make_data :
319 stream_id:stream_id ->
320 ?end_stream:bool ->
321 Cstruct.t ->
322 frame
323(** [make_data ~stream_id ?end_stream data] creates a DATA frame. *)
324
325val make_headers :
326 stream_id:stream_id ->
327 ?end_stream:bool ->
328 ?end_headers:bool ->
329 ?priority:priority ->
330 Cstruct.t ->
331 frame
332(** [make_headers ~stream_id ?end_stream ?end_headers ?priority block]
333 creates a HEADERS frame. *)
334
335val make_rst_stream :
336 stream_id:stream_id ->
337 error_code ->
338 frame
339(** [make_rst_stream ~stream_id code] creates a RST_STREAM frame. *)
340
341val make_settings :
342 ?ack:bool ->
343 setting list ->
344 frame
345(** [make_settings ?ack settings] creates a SETTINGS frame. *)
346
347val make_ping :
348 ?ack:bool ->
349 Cstruct.t ->
350 frame
351(** [make_ping ?ack data] creates a PING frame.
352 [data] must be exactly 8 bytes. *)
353
354val make_goaway :
355 last_stream_id:stream_id ->
356 error_code ->
357 ?debug:string ->
358 unit ->
359 frame
360(** [make_goaway ~last_stream_id code ?debug ()] creates a GOAWAY frame. *)
361
362val make_window_update :
363 stream_id:stream_id ->
364 int32 ->
365 frame
366(** [make_window_update ~stream_id increment] creates a WINDOW_UPDATE frame. *)
367
368val make_continuation :
369 stream_id:stream_id ->
370 ?end_headers:bool ->
371 Cstruct.t ->
372 frame
373(** [make_continuation ~stream_id ?end_headers block] creates a CONTINUATION frame. *)
374
375(** {1 Constants} *)
376
377val default_max_frame_size : int
378(** Default SETTINGS_MAX_FRAME_SIZE: 16384 (2^14). *)
379
380val max_max_frame_size : int
381(** Maximum SETTINGS_MAX_FRAME_SIZE: 16777215 (2^24 - 1). *)
382
383val default_initial_window_size : int32
384(** Default initial flow control window: 65535 (2^16 - 1). *)
385
386val max_window_size : int32
387(** Maximum flow control window: 2147483647 (2^31 - 1). *)
388
389val connection_preface : string
390(** HTTP/2 connection preface (magic string):
391 "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" *)
392
393val connection_preface_length : int
394(** Length of connection preface: 24 bytes. *)