···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** HTTP 100-Continue configuration
77+88+ Configuration for the HTTP 100-Continue protocol, which allows clients
99+ to check if the server will accept a request before sending a large body.
1010+ Per RFC 9110 Section 10.1.1 (Expect) and Section 15.2.1 (100 Continue). *)
1111+1212+type t = {
1313+ enabled : bool;
1414+ threshold : int64;
1515+ timeout : float;
1616+}
1717+1818+let default = {
1919+ enabled = true;
2020+ threshold = 1_048_576L; (* 1MB *)
2121+ timeout = 1.0; (* 1 second *)
2222+}
2323+2424+let make ?(enabled = true) ?(threshold = 1_048_576L) ?(timeout = 1.0) () =
2525+ { enabled; threshold; timeout }
2626+2727+let disabled = { enabled = false; threshold = 0L; timeout = 0.0 }
2828+2929+let enabled t = t.enabled
3030+let threshold t = t.threshold
3131+let timeout t = t.timeout
3232+3333+let should_use t body_size =
3434+ t.enabled && body_size >= t.threshold
3535+3636+let pp fmt t =
3737+ Format.fprintf fmt "@[<v 2>Expect_continue {@ \
3838+ enabled: %b@ \
3939+ threshold: %Ld bytes@ \
4040+ timeout: %.2fs@ \
4141+ }@]"
4242+ t.enabled
4343+ t.threshold
4444+ t.timeout
4545+4646+let to_string t = Format.asprintf "%a" pp t
+49
lib/expect_continue.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** HTTP 100-Continue configuration
77+88+ Configuration for the HTTP 100-Continue protocol, which allows clients
99+ to check if the server will accept a request before sending a large body.
1010+ Per RFC 9110 Section 10.1.1 (Expect) and Section 15.2.1 (100 Continue). *)
1111+1212+type t
1313+(** Abstract type representing HTTP 100-Continue configuration. *)
1414+1515+val default : t
1616+(** Default configuration:
1717+ - enabled: true
1818+ - threshold: 1MB
1919+ - timeout: 1.0s *)
2020+2121+val make :
2222+ ?enabled:bool ->
2323+ ?threshold:int64 ->
2424+ ?timeout:float ->
2525+ unit -> t
2626+(** Create custom 100-Continue configuration. All parameters are optional
2727+ and default to the values in {!default}. *)
2828+2929+val disabled : t
3030+(** Configuration with 100-Continue disabled. *)
3131+3232+val enabled : t -> bool
3333+(** Whether 100-continue is enabled. *)
3434+3535+val threshold : t -> int64
3636+(** Body size threshold in bytes to trigger 100-continue. *)
3737+3838+val timeout : t -> float
3939+(** Timeout in seconds to wait for 100 response. *)
4040+4141+val should_use : t -> int64 -> bool
4242+(** [should_use t body_size] returns [true] if 100-continue should be used
4343+ for a request with the given [body_size]. *)
4444+4545+val pp : Format.formatter -> t -> unit
4646+(** Pretty-printer for 100-Continue configuration. *)
4747+4848+val to_string : t -> string
4949+(** Convert configuration to a human-readable string. *)
+17-18
lib/http_client.ml
···99 {!Http_read} for response parsing, leveraging Eio's Buf_write and Buf_read
1010 for efficient I/O.
11111212- Types are imported from {!Http_types} and re-exported for API convenience. *)
1212+ Types are imported from domain-specific modules ({!Response_limits},
1313+ {!Expect_continue}) and re-exported for API convenience. *)
13141415let src = Logs.Src.create "requests.http_client" ~doc:"Low-level HTTP client"
1516module Log = (val Logs.src_log src : Logs.LOG)
16171718(** {1 Types}
18191919- Re-exported from {!Http_types} for API convenience.
2020- We open Http_types to bring record field names into scope. *)
2121-2222-open Http_types
2020+ Re-exported from domain-specific modules for API convenience. *)
23212424-type limits = Http_types.limits
2525-let default_limits = Http_types.default_limits
2222+type limits = Response_limits.t
2323+let default_limits = Response_limits.default
26242727-type expect_100_config = Http_types.expect_100_config
2828-let default_expect_100_config = Http_types.default_expect_100_config
2525+type expect_100_config = Expect_continue.t
2626+let default_expect_100_config = Expect_continue.default
29273028(** {1 Decompression Support} *)
3129···119117let check_decompression_limits ~limits ~compressed_size decompressed =
120118 let decompressed_size = Int64.of_int (String.length decompressed) in
121119 let compressed_size_i64 = Int64.of_int compressed_size in
120120+ let max_decompressed = Response_limits.max_decompressed_size limits in
122121123122 (* Check absolute size *)
124124- if decompressed_size > limits.max_decompressed_size then begin
123123+ if decompressed_size > max_decompressed then begin
125124 let ratio = Int64.to_float decompressed_size /. Int64.to_float compressed_size_i64 in
126125 raise (Error.err (Error.Decompression_bomb {
127127- limit = limits.max_decompressed_size;
126126+ limit = max_decompressed;
128127 ratio
129128 }))
130129 end;
···132131 (* Check ratio - only if compressed size is > 0 to avoid division by zero *)
133132 if compressed_size > 0 then begin
134133 let ratio = Int64.to_float decompressed_size /. Int64.to_float compressed_size_i64 in
135135- if ratio > limits.max_compression_ratio then
134134+ if ratio > Response_limits.max_compression_ratio limits then
136135 raise (Error.err (Error.Decompression_bomb {
137137- limit = limits.max_decompressed_size;
136136+ limit = max_decompressed;
138137 ratio
139138 }))
140139 end;
···281280282281 (* Determine if we should use 100-continue *)
283282 let use_100_continue =
284284- expect_100.enabled &&
285285- body_len >= expect_100.threshold &&
283283+ Expect_continue.enabled expect_100 &&
284284+ body_len >= Expect_continue.threshold expect_100 &&
286285 body_len > 0L &&
287286 not (Headers.mem "expect" headers) (* Don't override explicit Expect header *)
288287 in
···290289 if not use_100_continue then begin
291290 (* Standard request without 100-continue *)
292291 Log.debug (fun m -> m "100-continue not used (body_len=%Ld, threshold=%Ld, enabled=%b)"
293293- body_len expect_100.threshold expect_100.enabled);
292292+ body_len (Expect_continue.threshold expect_100) (Expect_continue.enabled expect_100));
294293 make_request ~limits ~sw ~method_ ~uri ~headers ~body flow
295294 end else begin
296295 Log.info (fun m -> m "Using 100-continue for large body (%Ld bytes)" body_len);
···311310 (* Wait for 100 Continue or error response with timeout *)
312311 let result =
313312 try
314314- Eio.Time.with_timeout_exn clock expect_100.timeout (fun () ->
315315- wait_for_100_continue ~limits ~timeout:expect_100.timeout flow
313313+ Eio.Time.with_timeout_exn clock (Expect_continue.timeout expect_100) (fun () ->
314314+ wait_for_100_continue ~limits ~timeout:(Expect_continue.timeout expect_100) flow
316315 )
317316 with Eio.Time.Timeout ->
318317 Log.debug (fun m -> m "100-continue timeout expired, sending body anyway");
+21-18
lib/http_read.ml
···13131414module Read = Eio.Buf_read
15151616-(** Import limits from Http_types - the single source of truth.
1717- We open Http_types to bring record field names into scope. *)
1818-open Http_types
1919-type limits = Http_types.limits
1616+(** Import limits from Response_limits module. *)
1717+type limits = Response_limits.t
20182119(** {1 Character Predicates} *)
2220···109107110108(** Parse all headers with size and count limits *)
111109let headers ~limits r =
110110+ let max_count = Response_limits.max_header_count limits in
111111+ let max_size = Response_limits.max_header_size limits in
112112 let rec loop acc count =
113113 (* Check header count limit *)
114114- if count >= limits.max_header_count then
114114+ if count >= max_count then
115115 raise (Error.err (Error.Headers_too_large {
116116- limit = limits.max_header_count;
116116+ limit = max_count;
117117 actual = count + 1
118118 }));
119119···126126 end else begin
127127 (* Check header line size limit *)
128128 let line_len = String.length name + String.length value + 2 in
129129- if line_len > limits.max_header_size then
129129+ if line_len > max_size then
130130 raise (Error.err (Error.Headers_too_large {
131131- limit = limits.max_header_size;
131131+ limit = max_size;
132132 actual = line_len
133133 }));
134134···141141142142(** Read a fixed-length body with size limit checking *)
143143let fixed_body ~limits ~length r =
144144+ let max_body = Response_limits.max_response_body_size limits in
144145 (* Check size limit before allocating *)
145145- if length > limits.max_response_body_size then
146146+ if length > max_body then
146147 raise (Error.err (Error.Body_too_large {
147147- limit = limits.max_response_body_size;
148148+ limit = max_body;
148149 actual = Some length
149150 }));
150151···203204(** Read a chunked transfer-encoded body with size limit checking *)
204205let chunked_body ~limits r =
205206 Log.debug (fun m -> m "Reading chunked body");
207207+ let max_body = Response_limits.max_response_body_size limits in
206208 let buf = Buffer.create 4096 in
207209 let total_size = ref 0L in
208210···217219 end else begin
218220 (* Check size limit before reading chunk *)
219221 let new_total = Int64.add !total_size (Int64.of_int size) in
220220- if new_total > limits.max_response_body_size then
222222+ if new_total > max_body then
221223 raise (Error.err (Error.Body_too_large {
222222- limit = limits.max_response_body_size;
224224+ limit = max_body;
223225 actual = Some new_total
224226 }));
225227···260262end
261263262264let fixed_body_stream ~limits ~length buf_read =
265265+ let max_body = Response_limits.max_response_body_size limits in
263266 (* Check size limit *)
264264- if length > limits.max_response_body_size then
267267+ if length > max_body then
265268 raise (Error.err (Error.Body_too_large {
266266- limit = limits.max_response_body_size;
269269+ limit = max_body;
267270 actual = Some length
268271 }));
269272···283286 buf_read : Read.t;
284287 mutable state : state;
285288 mutable total_read : int64;
286286- limits : limits;
289289+ max_body_size : int64;
287290 }
288291289292 let read_chunk_size t =
···315318 end else begin
316319 (* Check size limit *)
317320 let new_total = Int64.add t.total_read (Int64.of_int size) in
318318- if new_total > t.limits.max_response_body_size then
321321+ if new_total > t.max_body_size then
319322 raise (Error.err (Error.Body_too_large {
320320- limit = t.limits.max_response_body_size;
323323+ limit = t.max_body_size;
321324 actual = Some new_total
322325 }));
323326 t.state <- Reading_chunk size;
···352355 Chunked_body_source.buf_read;
353356 state = Reading_size;
354357 total_read = 0L;
355355- limits
358358+ max_body_size = Response_limits.max_response_body_size limits;
356359 } in
357360 let ops = Eio.Flow.Pi.source (module Chunked_body_source) in
358361 Eio.Resource.T (t, ops)
+3-3
lib/http_read.mli
···16161717(** {1 Response Limits}
18181919- This module uses {!Http_types.limits} from the shared types module. *)
1919+ This module uses {!Response_limits.t} for size limit configuration. *)
20202121-type limits = Http_types.limits
2222-(** Alias for {!Http_types.limits}. See {!Http_types} for field documentation. *)
2121+type limits = Response_limits.t
2222+(** Alias for {!Response_limits.t}. See {!Response_limits} for documentation. *)
23232424(** {1 Low-level Parsers} *)
2525
-48
lib/http_types.ml
···11-(*---------------------------------------------------------------------------
22- Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33- SPDX-License-Identifier: ISC
44- ---------------------------------------------------------------------------*)
55-66-(** Shared types for HTTP protocol handling
77-88- This module contains type definitions used across the HTTP client modules.
99- It serves as the single source of truth for types shared between
1010- {!Http_read}, {!Http_write}, and {!Http_client}. *)
1111-1212-(** {1 Response Limits}
1313-1414- Per Recommendation #2: Configurable limits for response body size,
1515- header count, and header length to prevent DoS attacks. *)
1616-1717-type limits = {
1818- max_response_body_size: int64; (** Maximum response body size in bytes (default: 100MB) *)
1919- max_header_size: int; (** Maximum size of a single header line (default: 16KB) *)
2020- max_header_count: int; (** Maximum number of headers (default: 100) *)
2121- max_decompressed_size: int64; (** Maximum decompressed size (default: 100MB) *)
2222- max_compression_ratio: float; (** Maximum compression ratio allowed (default: 100:1) *)
2323-}
2424-2525-let default_limits = {
2626- max_response_body_size = 104_857_600L; (* 100MB *)
2727- max_header_size = 16_384; (* 16KB *)
2828- max_header_count = 100;
2929- max_decompressed_size = 104_857_600L; (* 100MB *)
3030- max_compression_ratio = 100.0; (* 100:1 *)
3131-}
3232-3333-(** {1 HTTP 100-Continue Configuration}
3434-3535- Per Recommendation #7: HTTP 100-Continue Support for Large Uploads.
3636- RFC 9110 Section 10.1.1 (Expect) and Section 15.2.1 (100 Continue) *)
3737-3838-type expect_100_config = {
3939- enabled : bool; (** Whether to use 100-continue at all *)
4040- threshold : int64; (** Body size threshold to trigger 100-continue (default: 1MB) *)
4141- timeout : float; (** Timeout to wait for 100 response (default: 1.0s) *)
4242-}
4343-4444-let default_expect_100_config = {
4545- enabled = true;
4646- threshold = 1_048_576L; (* 1MB *)
4747- timeout = 1.0; (* 1 second *)
4848-}
-50
lib/http_types.mli
···11-(*---------------------------------------------------------------------------
22- Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33- SPDX-License-Identifier: ISC
44- ---------------------------------------------------------------------------*)
55-66-(** Shared types for HTTP protocol handling
77-88- This module contains type definitions used across the HTTP client modules.
99- It serves as the single source of truth for types shared between
1010- {!Http_read}, {!Http_write}, and {!Http_client}. *)
1111-1212-(** {1 Response Limits}
1313-1414- Configurable limits for response body size, header count, and header length
1515- to prevent DoS attacks. *)
1616-1717-type limits = {
1818- max_response_body_size: int64; (** Maximum response body size in bytes *)
1919- max_header_size: int; (** Maximum size of a single header line *)
2020- max_header_count: int; (** Maximum number of headers *)
2121- max_decompressed_size: int64; (** Maximum decompressed size *)
2222- max_compression_ratio: float; (** Maximum compression ratio allowed *)
2323-}
2424-(** Response size limits to prevent resource exhaustion. *)
2525-2626-val default_limits : limits
2727-(** Default limits:
2828- - max_response_body_size: 100MB
2929- - max_header_size: 16KB
3030- - max_header_count: 100
3131- - max_decompressed_size: 100MB
3232- - max_compression_ratio: 100:1 *)
3333-3434-(** {1 HTTP 100-Continue Configuration}
3535-3636- Configuration for the HTTP 100-Continue protocol, which allows clients
3737- to check if the server will accept a request before sending a large body. *)
3838-3939-type expect_100_config = {
4040- enabled : bool; (** Whether to use 100-continue at all *)
4141- threshold : int64; (** Body size threshold to trigger 100-continue *)
4242- timeout : float; (** Timeout to wait for 100 response in seconds *)
4343-}
4444-(** Configuration for HTTP 100-Continue support. *)
4545-4646-val default_expect_100_config : expect_100_config
4747-(** Default configuration:
4848- - enabled: true
4949- - threshold: 1MB
5050- - timeout: 1.0s *)
+6-5
lib/one.ml
···214214 ~timeout ~verify_tls ~tls_config ~min_tls_version in
215215216216 (* Build expect_100 config *)
217217- let expect_100_config = Http_types.{
218218- enabled = expect_100_continue;
219219- threshold = expect_100_continue_threshold;
220220- timeout = Option.bind timeout Timeout.expect_100_continue |> Option.value ~default:1.0;
221221- } in
217217+ let expect_100_config = Expect_continue.make
218218+ ~enabled:expect_100_continue
219219+ ~threshold:expect_100_continue_threshold
220220+ ~timeout:(Option.bind timeout Timeout.expect_100_continue |> Option.value ~default:1.0)
221221+ ()
222222+ in
222223223224 (* Make HTTP request using low-level client with 100-continue and optional auto-decompression *)
224225 let status, resp_headers, response_body_str =
+9-6
lib/requests.ml
···2121module Error = Error
2222module Retry = Retry
2323module Cache_control = Cache_control
2424+module Response_limits = Response_limits
2525+module Expect_continue = Expect_continue
24262527(** Minimum TLS version configuration.
2628 Per Recommendation #6: Allow enforcing minimum TLS version. *)
···5860 persist_cookies : bool;
5961 xdg : Xdge.t option;
6062 auto_decompress : bool;
6161- expect_100_continue : Http_client.expect_100_config; (** 100-continue configuration *)
6363+ expect_100_continue : Expect_continue.t; (** 100-continue configuration *)
62646365 (* Statistics - mutable but NOTE: when sessions are derived via record update
6466 syntax ({t with field = value}), these are copied not shared. Each derived
···177179 in
178180179181 (* Build expect_100_continue configuration *)
180180- let expect_100_config = Http_types.{
181181- enabled = expect_100_continue;
182182- threshold = expect_100_continue_threshold;
183183- timeout = Timeout.expect_100_continue timeout |> Option.value ~default:1.0;
184184- } in
182182+ let expect_100_config = Expect_continue.make
183183+ ~enabled:expect_100_continue
184184+ ~threshold:expect_100_continue_threshold
185185+ ~timeout:(Timeout.expect_100_continue timeout |> Option.value ~default:1.0)
186186+ ()
187187+ in
185188186189 T {
187190 sw;
+6
lib/requests.mli
···708708(** HTTP Cache-Control header parsing (RFC 9111) *)
709709module Cache_control = Cache_control
710710711711+(** HTTP response size limits for DoS prevention *)
712712+module Response_limits = Response_limits
713713+714714+(** HTTP 100-Continue configuration for large uploads *)
715715+module Expect_continue = Expect_continue
716716+711717(** {2 Logging} *)
712718713719(** Log source for the requests library.
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Response limits for HTTP protocol handling
77+88+ Configurable limits for response body size, header count, and header length
99+ to prevent DoS attacks. *)
1010+1111+type t
1212+(** Abstract type representing HTTP response limits. *)
1313+1414+val default : t
1515+(** Default limits:
1616+ - max_response_body_size: 100MB
1717+ - max_header_size: 16KB
1818+ - max_header_count: 100
1919+ - max_decompressed_size: 100MB
2020+ - max_compression_ratio: 100:1 *)
2121+2222+val make :
2323+ ?max_response_body_size:int64 ->
2424+ ?max_header_size:int ->
2525+ ?max_header_count:int ->
2626+ ?max_decompressed_size:int64 ->
2727+ ?max_compression_ratio:float ->
2828+ unit -> t
2929+(** Create custom response limits. All parameters are optional and default
3030+ to the values in {!default}. *)
3131+3232+val max_response_body_size : t -> int64
3333+(** Maximum response body size in bytes. *)
3434+3535+val max_header_size : t -> int
3636+(** Maximum size of a single header line in bytes. *)
3737+3838+val max_header_count : t -> int
3939+(** Maximum number of headers allowed. *)
4040+4141+val max_decompressed_size : t -> int64
4242+(** Maximum decompressed size in bytes. *)
4343+4444+val max_compression_ratio : t -> float
4545+(** Maximum compression ratio allowed (e.g., 100.0 means 100:1). *)
4646+4747+val pp : Format.formatter -> t -> unit
4848+(** Pretty-printer for response limits. *)
4949+5050+val to_string : t -> string
5151+(** Convert response limits to a human-readable string. *)