A batteries included HTTP/1.1 client in OCaml
at main 126 lines 5.6 kB view raw
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 *)