A batteries included HTTP/1.1 client in OCaml
at main 169 lines 7.1 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 response parsing using Eio.Buf_read combinators 7 8 This module provides efficient HTTP/1.1 response parsing using Eio's 9 buffered read API with parser combinators for clean, composable parsing. 10 11 Example: 12 {[ 13 let buf_read = Http_read.of_flow ~max_size:max_int flow in 14 let (status, headers, body) = Http_read.response ~limits buf_read 15 ]} *) 16 17(** {1 Response Limits} 18 19 This module uses {!Response_limits.t} for size limit configuration. *) 20 21type limits = Response_limits.t 22(** Alias for {!Response_limits.t}. See {!Response_limits} for documentation. *) 23 24(** {1 HTTP Version Type} 25 26 Per Recommendation #26: Expose HTTP version used for the response. *) 27 28type http_version = 29 | HTTP_1_0 (** HTTP/1.0 *) 30 | HTTP_1_1 (** HTTP/1.1 *) 31(** HTTP protocol version. Useful for debugging protocol negotiation 32 and monitoring HTTP/2 adoption (when supported). *) 33 34val http_version_to_string : http_version -> string 35(** [http_version_to_string v] returns "HTTP/1.0" or "HTTP/1.1". *) 36 37(** {1 Low-level Parsers} *) 38 39val http_version : Eio.Buf_read.t -> string 40(** [http_version r] parses HTTP version string (e.g., "HTTP/1.1"). *) 41 42val status_code : Eio.Buf_read.t -> int 43(** [status_code r] parses a 3-digit HTTP status code. 44 @raise Error if the status code is invalid. *) 45 46val status_line : Eio.Buf_read.t -> http_version * int 47(** [status_line r] parses a complete HTTP status line and returns 48 the HTTP version and status code as a tuple. 49 Validates that the HTTP version is 1.0 or 1.1. 50 @raise Error if the status line is invalid. *) 51 52(** {1 Header Parsing} *) 53 54val header_line : Eio.Buf_read.t -> string * string 55(** [header_line r] parses a single header line. 56 Returns [(name, value)] where name is lowercase. 57 Returns [("", "")] for the empty line that terminates headers. *) 58 59val headers : limits:limits -> Eio.Buf_read.t -> Headers.t 60(** [headers ~limits r] parses all headers until the terminating blank line. 61 Enforces header count and size limits. 62 @raise Error.Headers_too_large if limits are exceeded. *) 63 64(** {1 Body Parsing} *) 65 66val fixed_body : limits:limits -> length:int64 -> Eio.Buf_read.t -> string 67(** [fixed_body ~limits ~length r] reads exactly [length] bytes as the body. 68 @raise Error.Body_too_large if length exceeds limit. 69 @raise Error.Content_length_mismatch if EOF occurs before all bytes read. *) 70 71val chunked_body : limits:limits -> Eio.Buf_read.t -> string 72(** [chunked_body ~limits r] reads a chunked transfer-encoded body. 73 Handles chunk sizes, extensions, and trailers. 74 @raise Error.Body_too_large if total body size exceeds limit. *) 75 76(** {1 Transfer-Encoding Validation} *) 77 78val parse_transfer_encoding : string option -> string list 79(** [parse_transfer_encoding header] parses Transfer-Encoding header value 80 into a list of codings (all lowercase, in order). *) 81 82val validate_transfer_encoding : string list -> 83 [ `Chunked | `None | `Unsupported of string list ] 84(** [validate_transfer_encoding codings] validates Transfer-Encoding per RFC 9112 Section 6.1. 85 Returns [`Chunked] if chunked encoding should be used, [`None] if no body, 86 or [`Unsupported codings] for unsupported encodings without chunked. 87 @raise Error if chunked is not final encoding (RFC violation). *) 88 89val validate_no_transfer_encoding : 90 method_:Method.t option -> status:int -> string option -> bool 91(** [validate_no_transfer_encoding ~method_ ~status te] validates that 92 Transfer-Encoding is not present in responses that MUST NOT have it. 93 Per RFC 9112 Section 6.1, these include responses to HEAD, 1xx, 204, and 304. 94 If present, this logs a warning about the RFC violation. 95 @return true if Transfer-Encoding is present (violation), false otherwise *) 96 97(** {1 Trailer Header Parsing} *) 98 99val forbidden_trailer_headers : string list 100(** Headers that MUST NOT appear in trailers per RFC 9110 Section 6.5.1. 101 Includes: transfer-encoding, content-length, host, content-encoding, 102 content-type, content-range, trailer. *) 103 104val parse_trailers : limits:limits -> Eio.Buf_read.t -> Headers.t 105(** [parse_trailers ~limits r] parses trailer headers after final chunk. 106 Forbidden headers are logged and ignored. *) 107 108(** {1 Streaming Body Sources} *) 109 110val fixed_body_stream : limits:limits -> length:int64 -> 111 Eio.Buf_read.t -> Eio.Flow.source_ty Eio.Resource.t 112(** [fixed_body_stream ~limits ~length r] creates a flow source that reads 113 [length] bytes from [r]. Useful for large bodies to avoid loading 114 everything into memory at once. *) 115 116val chunked_body_stream : limits:limits -> 117 Eio.Buf_read.t -> Eio.Flow.source_ty Eio.Resource.t 118(** [chunked_body_stream ~limits r] creates a flow source that reads 119 chunked transfer-encoded data from [r]. Decodes chunks on-the-fly. *) 120 121(** {1 High-level Response Parsing} *) 122 123val response : limits:limits -> ?method_:Method.t -> Eio.Buf_read.t -> http_version * int * Headers.t * string 124(** [response ~limits ?method_ r] parses a complete HTTP response including: 125 - HTTP version 126 - Status code 127 - Headers 128 - Body (based on Transfer-Encoding or Content-Length) 129 130 Returns [(http_version, status, headers, body)]. 131 132 @param method_ The HTTP method of the request. Per 133 {{:https://datatracker.ietf.org/doc/html/rfc9110#section-6.4.1}RFC 9110 Section 6.4.1}, 134 certain responses have no body: 135 {ul 136 {- [`HEAD] - body is always empty regardless of Content-Length} 137 {- [`CONNECT] with 2xx - switches to tunnel mode, no body} 138 {- 1xx, 204, 304 - no content responses}} 139 140 This reads the entire body into memory. For large responses, 141 use {!response_stream} instead. *) 142 143(** {1 Streaming Response} *) 144 145type stream_response = { 146 http_version : http_version; (** HTTP protocol version *) 147 status : int; 148 headers : Headers.t; 149 body : [ `String of string 150 | `Stream of Eio.Flow.source_ty Eio.Resource.t 151 | `None ] 152} 153(** A parsed response with optional streaming body. 154 Per Recommendation #26: Includes HTTP version for debugging/monitoring. *) 155 156val response_stream : limits:limits -> ?method_:Method.t -> Eio.Buf_read.t -> stream_response 157(** [response_stream ~limits ?method_ r] parses status line and headers, then 158 returns a streaming body source instead of reading the body into memory. 159 Use this for large responses. 160 161 @param method_ The HTTP method of the request. Used to validate 162 that Transfer-Encoding is not present in responses that shouldn't have it 163 (HEAD requests). *) 164 165(** {1 Convenience Functions} *) 166 167val of_flow : ?initial_size:int -> max_size:int -> _ Eio.Flow.source -> Eio.Buf_read.t 168(** [of_flow ~max_size flow] creates a buffered reader from [flow]. 169 This is a thin wrapper around {!Eio.Buf_read.of_flow}. *)