A batteries included HTTP/1.1 client in OCaml

Consolidate expect_100_continue into single polymorphic variant

Replace two parameters (expect_100_continue:bool, expect_100_continue_threshold:int64)
with a single polymorphic variant parameter:

type config = [
| `Disabled (* Never use 100-continue *)
| `Always (* Always use for requests with bodies *)
| `Threshold of int64 (* Use for bodies >= n bytes *)
]

Default: `Threshold 1_048_576L (1MB) - same behavior as before.

This provides a cleaner API while maintaining full flexibility.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+113 -64
+30 -9
lib/expect_continue.ml
··· 9 9 to check if the server will accept a request before sending a large body. 10 10 Per RFC 9110 Section 10.1.1 (Expect) and Section 15.2.1 (100 Continue). *) 11 11 12 + (** User-facing configuration as a polymorphic variant *) 13 + type config = [ 14 + | `Disabled (** Never use 100-continue *) 15 + | `Always (** Always use 100-continue regardless of body size *) 16 + | `Threshold of int64 (** Use 100-continue for bodies >= threshold bytes *) 17 + ] 18 + 19 + (** Internal representation *) 12 20 type t = { 13 21 enabled : bool; 14 22 threshold : int64; 15 23 timeout : float; 16 24 } 25 + 26 + let default_threshold = 1_048_576L (* 1MB *) 17 27 18 28 let default = { 19 29 enabled = true; 20 - threshold = 1_048_576L; (* 1MB *) 30 + threshold = default_threshold; 21 31 timeout = 1.0; (* 1 second *) 22 32 } 33 + 34 + let of_config ?(timeout = 1.0) (config : config) : t = 35 + match config with 36 + | `Disabled -> { enabled = false; threshold = 0L; timeout } 37 + | `Always -> { enabled = true; threshold = 0L; timeout } 38 + | `Threshold n -> { enabled = true; threshold = n; timeout } 23 39 24 40 let make ?(enabled = true) ?(threshold = 1_048_576L) ?(timeout = 1.0) () = 25 41 { enabled; threshold; timeout } ··· 34 50 t.enabled && body_size >= t.threshold 35 51 36 52 let pp fmt t = 37 - Format.fprintf fmt "@[<v 2>Expect_continue {@ \ 38 - enabled: %b@ \ 39 - threshold: %Ld bytes@ \ 40 - timeout: %.2fs@ \ 41 - }@]" 42 - t.enabled 43 - t.threshold 44 - t.timeout 53 + if not t.enabled then 54 + Format.fprintf fmt "100-continue: disabled" 55 + else if t.threshold = 0L then 56 + Format.fprintf fmt "100-continue: always (timeout: %.2fs)" t.timeout 57 + else 58 + Format.fprintf fmt "100-continue: threshold %Ld bytes (timeout: %.2fs)" 59 + t.threshold t.timeout 45 60 46 61 let to_string t = Format.asprintf "%a" pp t 62 + 63 + let pp_config fmt (config : config) = 64 + match config with 65 + | `Disabled -> Format.fprintf fmt "`Disabled" 66 + | `Always -> Format.fprintf fmt "`Always" 67 + | `Threshold n -> Format.fprintf fmt "`Threshold %Ld" n
+50 -8
lib/expect_continue.mli
··· 7 7 8 8 Configuration for the HTTP 100-Continue protocol, which allows clients 9 9 to check if the server will accept a request before sending a large body. 10 - Per RFC 9110 Section 10.1.1 (Expect) and Section 15.2.1 (100 Continue). *) 10 + Per RFC 9110 Section 10.1.1 (Expect) and Section 15.2.1 (100 Continue). 11 + 12 + {2 Usage} 13 + 14 + The simplest way to configure 100-continue is with the polymorphic variant: 15 + {[ 16 + (* Use 100-continue for bodies >= 1MB (default) *) 17 + let session = Requests.create ~sw ~expect_100_continue:(`Threshold 1_048_576L) env 18 + 19 + (* Always use 100-continue *) 20 + let session = Requests.create ~sw ~expect_100_continue:`Always env 21 + 22 + (* Disable 100-continue *) 23 + let session = Requests.create ~sw ~expect_100_continue:`Disabled env 24 + ]} *) 25 + 26 + (** {1 Configuration Types} *) 27 + 28 + type config = [ 29 + | `Disabled (** Never use 100-continue *) 30 + | `Always (** Always use 100-continue regardless of body size *) 31 + | `Threshold of int64 (** Use 100-continue for bodies >= threshold bytes *) 32 + ] 33 + (** User-facing configuration as a polymorphic variant. 34 + 35 + - [`Disabled]: Never send Expect: 100-continue header 36 + - [`Always]: Always send Expect: 100-continue for requests with bodies 37 + - [`Threshold n]: Send Expect: 100-continue for bodies >= n bytes *) 11 38 12 39 type t 13 - (** Abstract type representing HTTP 100-Continue configuration. *) 40 + (** Internal configuration type with timeout. *) 41 + 42 + (** {1 Default Values} *) 43 + 44 + val default_threshold : int64 45 + (** Default threshold: 1MB (1_048_576 bytes) *) 14 46 15 47 val default : t 16 - (** Default configuration: 17 - - enabled: true 18 - - threshold: 1MB 19 - - timeout: 1.0s *) 48 + (** Default configuration: [`Threshold 1_048_576L] with 1.0s timeout *) 49 + 50 + val disabled : t 51 + (** Configuration with 100-Continue disabled. *) 52 + 53 + (** {1 Construction} *) 54 + 55 + val of_config : ?timeout:float -> config -> t 56 + (** [of_config ?timeout config] creates internal configuration from 57 + user-facing config. Timeout defaults to 1.0s. *) 20 58 21 59 val make : 22 60 ?enabled:bool -> ··· 26 64 (** Create custom 100-Continue configuration. All parameters are optional 27 65 and default to the values in {!default}. *) 28 66 29 - val disabled : t 30 - (** Configuration with 100-Continue disabled. *) 67 + (** {1 Accessors} *) 31 68 32 69 val enabled : t -> bool 33 70 (** Whether 100-continue is enabled. *) ··· 42 79 (** [should_use t body_size] returns [true] if 100-continue should be used 43 80 for a request with the given [body_size]. *) 44 81 82 + (** {1 Pretty Printing} *) 83 + 45 84 val pp : Format.formatter -> t -> unit 46 85 (** Pretty-printer for 100-Continue configuration. *) 47 86 48 87 val to_string : t -> string 49 88 (** Convert configuration to a human-readable string. *) 89 + 90 + val pp_config : Format.formatter -> config -> unit 91 + (** Pretty-printer for the user-facing config variant. *)
+15 -19
lib/one.ml
··· 90 90 ?(follow_redirects = true) ?(max_redirects = 10) 91 91 ?(verify_tls = true) ?tls_config ?(auto_decompress = true) 92 92 ?(min_tls_version = TLS_1_2) 93 - ?(expect_100_continue = true) ?(expect_100_continue_threshold = 1_048_576L) 93 + ?(expect_100_continue = `Threshold Expect_continue.default_threshold) 94 94 ?(allow_insecure_auth = false) 95 95 ?proxy 96 96 ~method_ url = ··· 148 148 149 149 let is_https = Uri.scheme uri_to_fetch = Some "https" in 150 150 151 - (* Build expect_100 config *) 152 - let expect_100_config = Expect_continue.make 153 - ~enabled:expect_100_continue 154 - ~threshold:expect_100_continue_threshold 155 - ~timeout:(Option.bind timeout Timeout.expect_100_continue |> Option.value ~default:1.0) 156 - () 157 - in 151 + (* Build expect_100 config from polymorphic variant *) 152 + let expect_100_timeout = Option.bind timeout Timeout.expect_100_continue |> Option.value ~default:1.0 in 153 + let expect_100_config = Expect_continue.of_config ~timeout:expect_100_timeout expect_100_continue in 158 154 159 155 (* Connect and make request based on proxy configuration *) 160 156 let status, resp_headers, response_body_str = ··· 291 287 request ~sw ~clock ~net ?headers ?auth ?timeout 292 288 ?follow_redirects ?max_redirects ?verify_tls ?tls_config ?min_tls_version 293 289 ?allow_insecure_auth ?proxy 294 - ~expect_100_continue:false (* GET has no body *) 290 + ~expect_100_continue:`Disabled (* GET has no body *) 295 291 ~method_:`GET url 296 292 297 293 let post ~sw ~clock ~net ?headers ?body ?auth ?timeout 298 294 ?verify_tls ?tls_config ?min_tls_version 299 - ?expect_100_continue ?expect_100_continue_threshold 295 + ?expect_100_continue 300 296 ?allow_insecure_auth ?proxy url = 301 297 request ~sw ~clock ~net ?headers ?body ?auth ?timeout 302 298 ?verify_tls ?tls_config ?min_tls_version 303 - ?expect_100_continue ?expect_100_continue_threshold 299 + ?expect_100_continue 304 300 ?allow_insecure_auth ?proxy ~method_:`POST url 305 301 306 302 let put ~sw ~clock ~net ?headers ?body ?auth ?timeout 307 303 ?verify_tls ?tls_config ?min_tls_version 308 - ?expect_100_continue ?expect_100_continue_threshold 304 + ?expect_100_continue 309 305 ?allow_insecure_auth ?proxy url = 310 306 request ~sw ~clock ~net ?headers ?body ?auth ?timeout 311 307 ?verify_tls ?tls_config ?min_tls_version 312 - ?expect_100_continue ?expect_100_continue_threshold 308 + ?expect_100_continue 313 309 ?allow_insecure_auth ?proxy ~method_:`PUT url 314 310 315 311 let delete ~sw ~clock ~net ?headers ?auth ?timeout ··· 318 314 request ~sw ~clock ~net ?headers ?auth ?timeout 319 315 ?verify_tls ?tls_config ?min_tls_version 320 316 ?allow_insecure_auth ?proxy 321 - ~expect_100_continue:false (* DELETE typically has no body *) 317 + ~expect_100_continue:`Disabled (* DELETE typically has no body *) 322 318 ~method_:`DELETE url 323 319 324 320 let head ~sw ~clock ~net ?headers ?auth ?timeout ··· 327 323 request ~sw ~clock ~net ?headers ?auth ?timeout 328 324 ?verify_tls ?tls_config ?min_tls_version 329 325 ?allow_insecure_auth ?proxy 330 - ~expect_100_continue:false (* HEAD has no body *) 326 + ~expect_100_continue:`Disabled (* HEAD has no body *) 331 327 ~method_:`HEAD url 332 328 333 329 let patch ~sw ~clock ~net ?headers ?body ?auth ?timeout 334 330 ?verify_tls ?tls_config ?min_tls_version 335 - ?expect_100_continue ?expect_100_continue_threshold 331 + ?expect_100_continue 336 332 ?allow_insecure_auth ?proxy url = 337 333 request ~sw ~clock ~net ?headers ?body ?auth ?timeout 338 334 ?verify_tls ?tls_config ?min_tls_version 339 - ?expect_100_continue ?expect_100_continue_threshold 335 + ?expect_100_continue 340 336 ?allow_insecure_auth ?proxy ~method_:`PATCH url 341 337 342 338 let upload ~sw ~clock ~net ?headers ?auth ?timeout ?method_ ?mime ?length 343 339 ?on_progress ?verify_tls ?tls_config ?min_tls_version 344 - ?(expect_100_continue = true) ?expect_100_continue_threshold 340 + ?expect_100_continue 345 341 ?allow_insecure_auth ?proxy ~source url = 346 342 let method_ = Option.value method_ ~default:`POST in 347 343 let mime = Option.value mime ~default:Mime.octet_stream in ··· 361 357 request ~sw ~clock ~net ?headers ~body ?auth ?timeout 362 358 ?verify_tls ?tls_config ?min_tls_version 363 359 ?allow_insecure_auth ?proxy 364 - ~expect_100_continue ?expect_100_continue_threshold ~method_ url 360 + ?expect_100_continue ~method_ url 365 361 366 362 let download ~sw ~clock ~net ?headers ?auth ?timeout ?on_progress 367 363 ?verify_tls ?tls_config ?min_tls_version ?allow_insecure_auth ?proxy url ~sink =
+9 -15
lib/one.mli
··· 77 77 ?tls_config:Tls.Config.client -> 78 78 ?auto_decompress:bool -> 79 79 ?min_tls_version:tls_version -> 80 - ?expect_100_continue:bool -> 81 - ?expect_100_continue_threshold:int64 -> 80 + ?expect_100_continue:Expect_continue.config -> 82 81 ?allow_insecure_auth:bool -> 83 82 ?proxy:Proxy.config -> 84 83 method_:Method.t -> ··· 86 85 Response.t 87 86 (** [request ~sw ~clock ~net ?headers ?body ?auth ?timeout ?follow_redirects 88 87 ?max_redirects ?verify_tls ?tls_config ?auto_decompress ?min_tls_version 89 - ?expect_100_continue ?expect_100_continue_threshold ?allow_insecure_auth 90 - ~method_ url] 88 + ?expect_100_continue ?allow_insecure_auth ~method_ url] 91 89 makes a single HTTP request without connection pooling. 92 90 93 91 Each call opens a new TCP connection (with TLS if https://), makes the ··· 106 104 @param tls_config Custom TLS configuration (default: system CA certs) 107 105 @param auto_decompress Whether to automatically decompress gzip/deflate responses (default: true) 108 106 @param min_tls_version Minimum TLS version to accept (default: TLS_1_2) 109 - @param expect_100_continue Use HTTP 100-continue for large bodies (default: true) 110 - @param expect_100_continue_threshold Body size threshold to trigger 100-continue (default: 1MB) 107 + @param expect_100_continue HTTP 100-continue configuration (default: [`Threshold 1_048_576L]). 108 + Use [`Disabled] to never send, [`Always] for all bodies, or [`Threshold n] for bodies >= n bytes. 111 109 @param allow_insecure_auth Allow Basic/Bearer/Digest auth over HTTP (default: false). 112 110 Per RFC 7617 Section 4 and RFC 6750 Section 5.1, these auth methods 113 111 MUST be used over TLS. Set to [true] only for testing environments. ··· 147 145 ?verify_tls:bool -> 148 146 ?tls_config:Tls.Config.client -> 149 147 ?min_tls_version:tls_version -> 150 - ?expect_100_continue:bool -> 151 - ?expect_100_continue_threshold:int64 -> 148 + ?expect_100_continue:Expect_continue.config -> 152 149 ?allow_insecure_auth:bool -> 153 150 ?proxy:Proxy.config -> 154 151 string -> ··· 166 163 ?verify_tls:bool -> 167 164 ?tls_config:Tls.Config.client -> 168 165 ?min_tls_version:tls_version -> 169 - ?expect_100_continue:bool -> 170 - ?expect_100_continue_threshold:int64 -> 166 + ?expect_100_continue:Expect_continue.config -> 171 167 ?allow_insecure_auth:bool -> 172 168 ?proxy:Proxy.config -> 173 169 string -> ··· 217 213 ?verify_tls:bool -> 218 214 ?tls_config:Tls.Config.client -> 219 215 ?min_tls_version:tls_version -> 220 - ?expect_100_continue:bool -> 221 - ?expect_100_continue_threshold:int64 -> 216 + ?expect_100_continue:Expect_continue.config -> 222 217 ?allow_insecure_auth:bool -> 223 218 ?proxy:Proxy.config -> 224 219 string -> ··· 239 234 ?verify_tls:bool -> 240 235 ?tls_config:Tls.Config.client -> 241 236 ?min_tls_version:tls_version -> 242 - ?expect_100_continue:bool -> 243 - ?expect_100_continue_threshold:int64 -> 237 + ?expect_100_continue:Expect_continue.config -> 244 238 ?allow_insecure_auth:bool -> 245 239 ?proxy:Proxy.config -> 246 240 source:Eio.Flow.source_ty Eio.Resource.t -> 247 241 string -> 248 242 Response.t 249 - (** Upload from stream with 100-continue support (default: enabled). 243 + (** Upload from stream with 100-continue support (default: [`Threshold 1MB]). 250 244 See {!request} for parameter details. *) 251 245 252 246 val download :
+4 -9
lib/requests.ml
··· 93 93 ?(persist_cookies = false) 94 94 ?xdg 95 95 ?(auto_decompress = true) 96 - ?(expect_100_continue = true) 97 - ?(expect_100_continue_threshold = 1_048_576L) (* 1MB *) 96 + ?(expect_100_continue = `Threshold Expect_continue.default_threshold) 98 97 ?base_url 99 98 ?(xsrf_cookie_name = Some "XSRF-TOKEN") (* Per Recommendation #24 *) 100 99 ?(xsrf_header_name = "X-XSRF-TOKEN") ··· 157 156 Cookeio_jar.create () 158 157 in 159 158 160 - (* Build expect_100_continue configuration *) 161 - let expect_100_config = Expect_continue.make 162 - ~enabled:expect_100_continue 163 - ~threshold:expect_100_continue_threshold 164 - ~timeout:(Timeout.expect_100_continue timeout |> Option.value ~default:1.0) 165 - () 166 - in 159 + (* Build expect_100_continue configuration from polymorphic variant *) 160 + let expect_100_timeout = Timeout.expect_100_continue timeout |> Option.value ~default:1.0 in 161 + let expect_100_config = Expect_continue.of_config ~timeout:expect_100_timeout expect_100_continue in 167 162 168 163 (* Normalize base_url: remove trailing slash for consistent path joining *) 169 164 let base_url = Option.map (fun url ->
+5 -4
lib/requests.mli
··· 255 255 ?persist_cookies:bool -> 256 256 ?xdg:Xdge.t -> 257 257 ?auto_decompress:bool -> 258 - ?expect_100_continue:bool -> 259 - ?expect_100_continue_threshold:int64 -> 258 + ?expect_100_continue:Expect_continue.config -> 260 259 ?base_url:string -> 261 260 ?xsrf_cookie_name:string option -> 262 261 ?xsrf_header_name:string -> ··· 286 285 @param persist_cookies Whether to persist cookies to disk via {!Cookeio_jar} (default: false) 287 286 @param xdg {!Xdge.t} XDG directory context for cookies (required if persist_cookies=true) 288 287 @param auto_decompress Whether to automatically decompress gzip/deflate responses (default: true) 289 - @param expect_100_continue Whether to use HTTP 100-continue for large uploads (default: true) 290 - @param expect_100_continue_threshold Body size threshold to trigger 100-continue in bytes (default: 1MB) 288 + @param expect_100_continue HTTP 100-continue configuration (default: [`Threshold 1_048_576L]). 289 + Use [`Disabled] to never send Expect: 100-continue, 290 + [`Always] to always send it for requests with bodies, or 291 + [`Threshold n] to send it only for bodies >= n bytes. 291 292 @param base_url Base URL for relative paths (per Recommendation #11). Relative URLs are resolved against this. 292 293 @param xsrf_cookie_name Cookie name to extract XSRF token from (default: Some "XSRF-TOKEN", per Recommendation #24). Set to None to disable. 293 294 @param xsrf_header_name Header name to inject XSRF token into (default: "X-XSRF-TOKEN")