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 request serialization using Eio.Buf_write
7
8 This module provides efficient HTTP/1.1 request serialization using Eio's
9 buffered write API. It avoids intermediate string allocations by writing
10 directly to the output buffer.
11
12 Example:
13 {[
14 Http_write.with_flow flow (fun w ->
15 Http_write.request w ~sw ~method_:`GET ~uri
16 ~headers:Headers.empty ~body:Body.empty
17 )
18 ]} *)
19
20(** {1 Low-level Writers} *)
21
22val crlf : Eio.Buf_write.t -> unit
23(** [crlf w] writes a CRLF line terminator ("\r\n") to [w]. *)
24
25val request_line : Eio.Buf_write.t -> method_:string -> uri:Uri.t -> unit
26(** [request_line w ~method_ ~uri] writes an HTTP request line.
27 For example: "GET /path?query HTTP/1.1\r\n" *)
28
29val header : Eio.Buf_write.t -> name:string -> value:string -> unit
30(** [header w ~name ~value] writes a single header line.
31 For example: "Content-Type: application/json\r\n" *)
32
33val headers : Eio.Buf_write.t -> Headers.t -> unit
34(** [headers w hdrs] writes all headers from [hdrs], followed by
35 a blank line (CRLF) to terminate the headers section. *)
36
37(** {1 Request Headers} *)
38
39val request_headers : Eio.Buf_write.t -> method_:string -> uri:Uri.t ->
40 headers:Headers.t -> content_length:int64 option -> unit
41(** [request_headers w ~method_ ~uri ~headers ~content_length] writes a complete
42 HTTP request header section, including:
43 - Request line (method, path, HTTP/1.1)
44 - Host header (extracted from URI if not present)
45 - Connection: keep-alive (if not present)
46 - Content-Length (if [content_length] provided and > 0)
47 - All headers from [headers]
48 - Terminating blank line *)
49
50val request_headers_only : Eio.Buf_write.t -> method_:Method.t -> uri:Uri.t ->
51 headers:Headers.t -> content_length:int64 option -> unit
52(** [request_headers_only] is like {!request_headers} but takes a [Method.t]
53 instead of a string. Used for 100-continue flow where headers are sent first. *)
54
55(** {1 Body Writing} *)
56
57val body_string : Eio.Buf_write.t -> string -> unit
58(** [body_string w s] writes string [s] as the request body.
59 Does nothing if [s] is empty. *)
60
61val body_stream : Eio.Buf_write.t -> Eio.Flow.source_ty Eio.Resource.t -> unit
62(** [body_stream w source] copies data from [source] to [w] until EOF.
63 Uses 8KB chunks for efficiency. The caller must ensure Content-Length
64 is set correctly in headers. *)
65
66val body_chunked : Eio.Buf_write.t -> Eio.Flow.source_ty Eio.Resource.t -> unit
67(** [body_chunked w source] writes data from [source] using HTTP chunked
68 transfer encoding. Each chunk is prefixed with its size in hex,
69 followed by CRLF, the data, and another CRLF. Ends with "0\r\n\r\n". *)
70
71(** {1 High-level Request Writing} *)
72
73val request : Eio.Buf_write.t -> sw:Eio.Switch.t -> method_:Method.t ->
74 uri:Uri.t -> headers:Headers.t -> body:Body.t -> unit
75(** [request w ~sw ~method_ ~uri ~headers ~body] writes a complete HTTP request
76 including headers and body. Automatically handles:
77 - Content-Type header from body
78 - Content-Length header for sized bodies
79 - Transfer-Encoding: chunked for unsized streams
80 - Multipart body encoding *)
81
82(** {1 Convenience Wrappers} *)
83
84val with_flow : ?initial_size:int -> _ Eio.Flow.sink ->
85 (Eio.Buf_write.t -> 'a) -> 'a
86(** [with_flow flow fn] runs [fn writer] where [writer] is a buffer that
87 flushes to [flow]. Data is automatically flushed when [fn] returns.
88
89 This is a thin wrapper around {!Eio.Buf_write.with_flow}.
90
91 {b Note:} This function creates an internal switch and may cause issues
92 with nested fibers. Consider using {!write_and_flush} instead. *)
93
94val write_and_flush : ?initial_size:int -> _ Eio.Flow.sink ->
95 (Eio.Buf_write.t -> unit) -> unit
96(** [write_and_flush flow fn] runs [fn writer] where [writer] is a buffer,
97 then serializes all written data to a string and copies it to [flow].
98
99 Unlike {!with_flow}, this does not create a nested switch and is safe
100 to use in complex fiber hierarchies. The tradeoff is that the entire
101 request is buffered in memory before being written. *)
102
103(** {1 Proxy Request Writing} *)
104
105val request_line_absolute : Eio.Buf_write.t -> method_:string -> uri:Uri.t -> unit
106(** [request_line_absolute w ~method_ ~uri] writes an HTTP request line
107 using absolute-URI form for proxy requests.
108 Per RFC 9112 Section 3.2.2: "A client MUST send a request-line with
109 absolute-form as the request-target when making a request to a proxy."
110 For example: "GET http://www.example.com/path HTTP/1.1\r\n" *)
111
112val request_via_proxy : Eio.Buf_write.t -> sw:Eio.Switch.t -> method_:Method.t ->
113 uri:Uri.t -> headers:Headers.t -> body:Body.t ->
114 proxy_auth:string option -> unit
115(** [request_via_proxy w ~sw ~method_ ~uri ~headers ~body ~proxy_auth]
116 writes a complete HTTP request using absolute-URI form for proxying.
117
118 Per RFC 9112 Section 3.2.2, when sending a request to a proxy for an
119 HTTP URL (not HTTPS), the client MUST use the absolute-URI form:
120 {v
121 GET http://www.example.com/path HTTP/1.1
122 Host: www.example.com
123 Proxy-Authorization: Basic ...
124 v}
125
126 @param proxy_auth Optional proxy authentication to add as Proxy-Authorization header *)