···5353 let doc = "Basic authentication in USER:PASSWORD format" in
5454 Arg.(value & opt (some string) None & info ["u"; "user"] ~docv:"USER:PASS" ~doc)
55555656+let allow_insecure_auth =
5757+ let doc = "Allow basic authentication over HTTP (insecure, for testing only)" in
5858+ Arg.(value & flag & info ["allow-insecure-auth"] ~doc)
5959+5660let show_progress =
5761 let doc = "Show progress bar for downloads" in
5862 Arg.(value & flag & info ["progress-bar"] ~doc)
···211215(* Main function using Requests with concurrent fetching *)
212216let run_request env sw persist_cookies verify_tls timeout follow_redirects max_redirects
213217 method_ urls headers data json_data output include_headers head
214214- auth _show_progress () =
218218+ auth allow_insecure_auth _show_progress () =
215219216220 (* Log levels are already set by setup_log via Logs_cli *)
217221···221225 (* Create requests instance with configuration *)
222226 let timeout_obj = Option.map (fun t -> Requests.Timeout.create ~total:t ()) timeout in
223227 let req = Requests.create ~sw ~xdg ~persist_cookies ~verify_tls
224224- ~follow_redirects ~max_redirects ?timeout:timeout_obj env in
228228+ ~follow_redirects ~max_redirects ~allow_insecure_auth ?timeout:timeout_obj env in
225229226230 (* Set authentication if provided *)
227231 let req = match auth with
···301305302306(* Main entry point *)
303307let main method_ urls headers data json_data output include_headers head
304304- auth show_progress persist_cookies_ws verify_tls_ws
308308+ auth allow_insecure_auth show_progress persist_cookies_ws verify_tls_ws
305309 timeout_ws follow_redirects_ws max_redirects_ws () =
306310307311 (* Extract values from with_source wrappers *)
···317321318322 run_request env sw persist_cookies verify_tls timeout follow_redirects max_redirects
319323 method_ urls headers data json_data output include_headers head auth
320320- show_progress ()
324324+ allow_insecure_auth show_progress ()
321325322326(* Command-line interface *)
323327let cmd =
···377381 let combined_term =
378382 Term.(const main $ http_method $ urls $ headers $ data $ json_data $
379383 output_file $ include_headers $ head $ auth $
380380- show_progress $
384384+ allow_insecure_auth $ show_progress $
381385 Requests.Cmd.persist_cookies_term app_name $
382386 Requests.Cmd.verify_tls_term app_name $
383387 Requests.Cmd.timeout_term app_name $
+14-10
examples/session_example.ml
···66open Eio
7788let () =
99+ Mirage_crypto_rng_unix.use_default ();
910 Eio_main.run @@ fun env ->
1010- Mirage_crypto_rng_eio.run (module Mirage_crypto_rng.Fortuna) env @@ fun () ->
1111 Switch.run @@ fun sw ->
12121313 (* Example 1: Basic GET request *)
···111111 (* Example 10: Error handling *)
112112 Printf.printf "\n=== Example 10: Error Handling ===\n%!";
113113 (try
114114- let _resp = Requests.get req "https://httpbin.org/status/404" in
115115- Printf.printf "Got 404 response (no exception thrown)\n%!"
114114+ let resp = Requests.get req "https://httpbin.org/status/404" in
115115+ (* By default, 4xx/5xx status codes don't raise exceptions *)
116116+ if Requests.Response.ok resp then
117117+ Printf.printf "Success\n%!"
118118+ else
119119+ Printf.printf "Got %d response (no exception by default)\n%!"
120120+ (Requests.Response.status_code resp);
121121+ (* Use raise_for_status to get exception behavior *)
122122+ let _resp = Requests.Response.raise_for_status resp in
123123+ ()
116124 with
117117- | Requests.Error.HTTPError { status; url; _ } ->
118118- Printf.printf "HTTP Error: %d for %s\n%!" status url
119119- | Requests.Error.Timeout ->
120120- Printf.printf "Request timed out\n%!"
121121- | Requests.Error.ConnectionError msg ->
122122- Printf.printf "Connection error: %s\n%!" msg
125125+ | Eio.Io (Requests.Error.E (Requests.Error.Http_error_status _), _) ->
126126+ Printf.printf "HTTP error status raised via raise_for_status\n%!"
123127 | exn ->
124128 Printf.printf "Other error: %s\n%!" (Printexc.to_string exn));
125129···133137 let resp11 = Requests.get req_timeout "https://httpbin.org/delay/2" in
134138 Printf.printf "Timeout test completed: %d\n%!" (Requests.Response.status_code resp11)
135139 with
136136- | Requests.Error.Timeout ->
140140+ | Eio.Time.Timeout ->
137141 Printf.printf "Request correctly timed out\n%!"
138142 | exn ->
139143 Printf.printf "Other timeout error: %s\n%!" (Printexc.to_string exn));
+14-1
lib/auth.mli
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** Authentication mechanisms *)
66+(** HTTP authentication mechanisms
77+88+ This module provides authentication schemes for HTTP requests:
99+1010+ - {b Basic}: {{:https://datatracker.ietf.org/doc/html/rfc7617}RFC 7617} - Base64 username:password
1111+ - {b Bearer}: {{:https://datatracker.ietf.org/doc/html/rfc6750}RFC 6750} - OAuth 2.0 tokens
1212+ - {b Digest}: {{:https://datatracker.ietf.org/doc/html/rfc7616}RFC 7616} - Challenge-response with MD5/SHA-256
1313+1414+ {2 Security}
1515+1616+ Per {{:https://datatracker.ietf.org/doc/html/rfc7617#section-4}RFC 7617 Section 4}} and
1717+ {{:https://datatracker.ietf.org/doc/html/rfc6750#section-5.1}RFC 6750 Section 5.1}},
1818+ Basic, Bearer, and Digest authentication transmit credentials that MUST be
1919+ protected by TLS. The library enforces HTTPS by default for these schemes. *)
720821(** Log source for authentication operations *)
922val src : Logs.Src.t
+9-3
lib/headers.mli
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** HTTP headers management with case-insensitive keys
66+(** HTTP header field handling per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-5}RFC 9110 Section 5}
7788 This module provides an efficient implementation of HTTP headers with
99- case-insensitive header names as per RFC 7230. Headers can have multiple
1010- values for the same key (e.g., multiple Set-Cookie headers).
99+ case-insensitive field names per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-5.1}RFC 9110 Section 5.1}}.
1010+ Headers can have multiple values for the same field name (e.g., Set-Cookie).
1111+1212+ {2 Security}
1313+1414+ Header names and values are validated to prevent HTTP header injection
1515+ attacks. CR and LF characters are rejected per
1616+ {{:https://datatracker.ietf.org/doc/html/rfc9110#section-5.5}RFC 9110 Section 5.5}}.
11171218 {2 Examples}
1319
+3-3
lib/http_client.ml
···177177178178 (* Read response using Buf_read *)
179179 let buf_read = Http_read.of_flow flow ~max_size:max_int in
180180- let (_version, status, headers, body) = Http_read.response ~limits buf_read in
180180+ let (_version, status, headers, body) = Http_read.response ~limits ~method_ buf_read in
181181 (status, headers, body)
182182183183(** Make HTTP request with optional auto-decompression *)
···336336337337 (* Read final response *)
338338 let buf_read = Http_read.of_flow flow ~max_size:max_int in
339339- let (_version, status, headers, body) = Http_read.response ~limits buf_read in
339339+ let (_version, status, headers, body) = Http_read.response ~limits ~method_ buf_read in
340340 (status, headers, body)
341341342342 | Rejected (status, resp_headers, resp_body_str) ->
···361361362362 (* Read response *)
363363 let buf_read = Http_read.of_flow flow ~max_size:max_int in
364364- let (_version, status, headers, body) = Http_read.response ~limits buf_read in
364364+ let (_version, status, headers, body) = Http_read.response ~limits ~method_ buf_read in
365365 (status, headers, body)
366366 end
367367
+45-27
lib/http_read.ml
···376376377377(** {1 High-level Response Parsing} *)
378378379379+(** Check if response should have no body per RFC 9110.
380380+ Per RFC 9110 Section 6.4.1:
381381+ - Any response to a HEAD request
382382+ - Any 1xx (Informational) response
383383+ - 204 (No Content) response
384384+ - 304 (Not Modified) response *)
385385+let response_has_no_body ~method_ ~status =
386386+ (method_ = Some `HEAD) ||
387387+ (status >= 100 && status < 200) ||
388388+ status = 204 ||
389389+ status = 304
390390+379391(** Parse complete response (status + headers + body) to string *)
380380-let response ~limits r =
392392+let response ~limits ?method_ r =
381393 let (version, status) = status_line r in
382394 let hdrs = headers ~limits r in
383395384384- (* Determine how to read body *)
385385- let transfer_encoding = Headers.get "transfer-encoding" hdrs in
386386- let content_length = Headers.get "content-length" hdrs |> Option.map Int64.of_string in
396396+ (* Per RFC 9110 Section 6.4.1: Certain responses MUST NOT have a body *)
397397+ if response_has_no_body ~method_ ~status then begin
398398+ Log.debug (fun m -> m "Response has no body (HEAD, 1xx, 204, or 304)");
399399+ (version, status, hdrs, "")
400400+ end else begin
401401+ (* Determine how to read body *)
402402+ let transfer_encoding = Headers.get "transfer-encoding" hdrs in
403403+ let content_length = Headers.get "content-length" hdrs |> Option.map Int64.of_string in
387404388388- (* Per RFC 9112 Section 6.3: When both Transfer-Encoding and Content-Length
389389- are present, Transfer-Encoding takes precedence. The presence of both
390390- headers is a potential HTTP request smuggling attack indicator. *)
391391- let body = match transfer_encoding, content_length with
392392- | Some te, Some _ when String.lowercase_ascii te |> String.trim = "chunked" ->
393393- (* Both headers present - log warning per RFC 9112 Section 6.3 *)
394394- Log.warn (fun m -> m "Both Transfer-Encoding and Content-Length present - \
395395- ignoring Content-Length per RFC 9112 (potential attack indicator)");
396396- chunked_body ~limits r
397397- | Some te, None when String.lowercase_ascii te |> String.trim = "chunked" ->
398398- Log.debug (fun m -> m "Reading chunked response body");
399399- chunked_body ~limits r
400400- | _, Some len ->
401401- Log.debug (fun m -> m "Reading fixed-length response body (%Ld bytes)" len);
402402- fixed_body ~limits ~length:len r
403403- | Some other_te, None ->
404404- Log.warn (fun m -> m "Unsupported transfer-encoding: %s, assuming no body" other_te);
405405- ""
406406- | None, None ->
407407- Log.debug (fun m -> m "No body indicated");
408408- ""
409409- in
405405+ (* Per RFC 9112 Section 6.3: When both Transfer-Encoding and Content-Length
406406+ are present, Transfer-Encoding takes precedence. The presence of both
407407+ headers is a potential HTTP request smuggling attack indicator. *)
408408+ let body = match transfer_encoding, content_length with
409409+ | Some te, Some _ when String.lowercase_ascii te |> String.trim = "chunked" ->
410410+ (* Both headers present - log warning per RFC 9112 Section 6.3 *)
411411+ Log.warn (fun m -> m "Both Transfer-Encoding and Content-Length present - \
412412+ ignoring Content-Length per RFC 9112 (potential attack indicator)");
413413+ chunked_body ~limits r
414414+ | Some te, None when String.lowercase_ascii te |> String.trim = "chunked" ->
415415+ Log.debug (fun m -> m "Reading chunked response body");
416416+ chunked_body ~limits r
417417+ | _, Some len ->
418418+ Log.debug (fun m -> m "Reading fixed-length response body (%Ld bytes)" len);
419419+ fixed_body ~limits ~length:len r
420420+ | Some other_te, None ->
421421+ Log.warn (fun m -> m "Unsupported transfer-encoding: %s, assuming no body" other_te);
422422+ ""
423423+ | None, None ->
424424+ Log.debug (fun m -> m "No body indicated");
425425+ ""
426426+ in
410427411411- (version, status, hdrs, body)
428428+ (version, status, hdrs, body)
429429+ end
412430413431(** Response with streaming body *)
414432type stream_response = {
+6-2
lib/http_read.mli
···88888989(** {1 High-level Response Parsing} *)
90909191-val response : limits:limits -> Eio.Buf_read.t -> http_version * int * Headers.t * string
9292-(** [response ~limits r] parses a complete HTTP response including:
9191+val response : limits:limits -> ?method_:Method.t -> Eio.Buf_read.t -> http_version * int * Headers.t * string
9292+(** [response ~limits ?method_ r] parses a complete HTTP response including:
9393 - HTTP version
9494 - Status code
9595 - Headers
9696 - Body (based on Transfer-Encoding or Content-Length)
97979898 Returns [(http_version, status, headers, body)].
9999+100100+ @param method_ The HTTP method of the request. When [`HEAD], the body
101101+ is always empty regardless of Content-Length header (per RFC 9110
102102+ Section 9.3.2). Similarly for 1xx, 204, and 304 responses.
99103100104 This reads the entire body into memory. For large responses,
101105 use {!response_stream} instead. *)
+15-1
lib/method.mli
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** HTTP methods following RFC 7231 *)
66+(** HTTP request methods per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-9}RFC 9110 Section 9}
77+88+ HTTP methods indicate the desired action to be performed on a resource.
99+ The method token is case-sensitive.
1010+1111+ {2 Safe Methods}
1212+1313+ Methods are considered "safe" if their semantics are read-only (GET, HEAD,
1414+ OPTIONS, TRACE). Per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-9.2.1}RFC 9110 Section 9.2.1}}.
1515+1616+ {2 Idempotent Methods}
1717+1818+ A method is "idempotent" if multiple identical requests have the same effect
1919+ as a single request (GET, HEAD, PUT, DELETE, OPTIONS, TRACE).
2020+ Per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-9.2.2}RFC 9110 Section 9.2.2}. *)
721822(** Log source for method operations *)
923val src : Logs.Src.t
+1-1
lib/one.ml
···266266 let limits = Response_limits.default in
267267 let buf_read = Http_read.of_flow ~max_size:65536 flow in
268268 let (_version, status, resp_headers, body_str) =
269269- Http_read.response ~limits buf_read in
269269+ Http_read.response ~limits ~method_ buf_read in
270270 (* Handle decompression if enabled *)
271271 let body_str =
272272 if auto_decompress then
+6-2
lib/one.mli
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** One-shot HTTP client for stateless requests
66+(** One-shot HTTP/1.1 client for stateless requests
7788 The One module provides a stateless HTTP client for single requests without
99 session state like cookies, connection pooling, or persistent configuration.
1010- Each request opens a new connection that is closed after use.
1010+ Each request opens a new TCP connection (with TLS for https://) that is
1111+ closed when the Eio switch closes.
1212+1313+ Implements {{:https://datatracker.ietf.org/doc/html/rfc9112}RFC 9112}} (HTTP/1.1)
1414+ with {{:https://datatracker.ietf.org/doc/html/rfc9110}RFC 9110}} semantics.
11151216 For stateful requests with automatic cookie handling, connection pooling,
1317 and persistent configuration, use the main {!Requests} module instead.
+6-27
lib/requests.ml
···7171 xsrf_cookie_name : string option; (** Per Recommendation #24: XSRF cookie name *)
7272 xsrf_header_name : string; (** Per Recommendation #24: XSRF header name *)
7373 proxy : Proxy.config option; (** HTTP/HTTPS proxy configuration *)
7474+ allow_insecure_auth : bool; (** Allow auth over HTTP for dev/testing *)
74757576 (* Statistics - mutable but NOTE: when sessions are derived via record update
7677 syntax ({t with field = value}), these are copied not shared. Each derived
···107108 ?(xsrf_cookie_name = Some "XSRF-TOKEN") (* Per Recommendation #24 *)
108109 ?(xsrf_header_name = "X-XSRF-TOKEN")
109110 ?proxy
111111+ ?(allow_insecure_auth = false)
110112 env =
111113112114 let clock = env#clock in
···231233 xsrf_cookie_name;
232234 xsrf_header_name;
233235 proxy;
236236+ allow_insecure_auth;
234237 requests_made = 0;
235238 total_time = 0.0;
236239 retries_count = 0;
···450453 | None -> t.auth
451454 in
452455453453- (* Apply auth *)
456456+ (* Apply auth with HTTPS validation per RFC 7617/6750 *)
454457 let headers = match auth with
455458 | Some a ->
456459 Log.debug (fun m -> m "Applying authentication");
457457- Auth.apply a headers
460460+ Auth.apply_secure ~allow_insecure_auth:t.allow_insecure_auth ~url a headers
458461 | None -> headers
459462 in
460463···602605 let limits = Response_limits.default in
603606 let buf_read = Http_read.of_flow ~max_size:65536 flow in
604607 let (_version, status, resp_headers, body_str) =
605605- Http_read.response ~limits buf_read in
608608+ Http_read.response ~limits ~method_ buf_read in
606609 (* Handle decompression if enabled *)
607610 let body_str =
608611 if t.auto_decompress then
···13681371 set_tls_tracing_level Logs.Warning
13691372end
1370137313711371-(** {1 Module-Level Convenience Functions}
13721372-13731373- These functions perform one-off requests without creating a session.
13741374- They are thin wrappers around {!One} module functions.
13751375- For multiple requests to the same hosts, prefer creating a session with {!create}
13761376- to benefit from connection pooling and cookie persistence. *)
13771377-13781378-let simple_get ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?auth ?timeout url =
13791379- One.get ~sw ~clock:env#clock ~net:env#net ?headers ?auth ?timeout url
13801380-13811381-let simple_post ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?body ?auth ?timeout url =
13821382- One.post ~sw ~clock:env#clock ~net:env#net ?headers ?body ?auth ?timeout url
13831383-13841384-let simple_put ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?body ?auth ?timeout url =
13851385- One.put ~sw ~clock:env#clock ~net:env#net ?headers ?body ?auth ?timeout url
13861386-13871387-let simple_patch ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?body ?auth ?timeout url =
13881388- One.patch ~sw ~clock:env#clock ~net:env#net ?headers ?body ?auth ?timeout url
13891389-13901390-let simple_delete ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?auth ?timeout url =
13911391- One.delete ~sw ~clock:env#clock ~net:env#net ?headers ?auth ?timeout url
13921392-13931393-let simple_head ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?auth ?timeout url =
13941394- One.head ~sw ~clock:env#clock ~net:env#net ?headers ?auth ?timeout url
+87-133
lib/requests.mli
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** Requests - A modern HTTP client library for OCaml
66+(** Requests - A modern HTTP/1.1 client library for OCaml
7788 Requests is an HTTP client library for OCaml inspired by Python's requests
99 and urllib3 libraries. It provides a simple, intuitive API for making HTTP
1010 requests while handling complexities like TLS configuration, connection
1111 pooling, retries, and cookie management.
12121313- {2 High-Level API}
1313+ The library implements {{:https://datatracker.ietf.org/doc/html/rfc9110}RFC 9110}
1414+ (HTTP Semantics) and {{:https://datatracker.ietf.org/doc/html/rfc9112}RFC 9112}
1515+ (HTTP/1.1).
1616+1717+ {2 Choosing an API}
14181515- The Requests library offers two main ways to make HTTP requests:
1919+ The library offers two APIs optimized for different use cases:
16201717- {b 1. Main API} (Recommended for most use cases)
2121+ {3 Session API (Requests.t)}
18221919- The main API maintains state across requests, handles cookies automatically,
2020- spawns requests in concurrent fibers, and provides a simple interface:
2323+ Use {!Requests.create} when you need:
2424+ - {b Cookie persistence} across requests (automatic session handling)
2525+ - {b Connection pooling} for efficient reuse of TCP/TLS connections
2626+ - {b Shared authentication} configured once for all requests
2727+ - {b Automatic retry handling} with exponential backoff
2828+ - {b Base URL support} for API clients ({{:https://datatracker.ietf.org/doc/html/rfc3986#section-5}RFC 3986 Section 5})
21292230 {[
2331 open Eio_main
24322533 let () = run @@ fun env ->
2626- Switch.run @@ fun sw ->
3434+ Eio.Switch.run @@ fun sw ->
27352828- (* Create a requests instance *)
2929- let req = Requests.create ~sw env in
3636+ (* Create a session with connection pooling *)
3737+ let session = Requests.create ~sw env in
30383131- (* Configure authentication once *)
3232- Requests.set_auth req (Requests.Auth.bearer "your-token");
3939+ (* Configure authentication once for all requests *)
4040+ let session = Requests.set_auth session (Requests.Auth.bearer "your-token") in
33413434- (* Make concurrent requests using Fiber.both *)
4242+ (* Make concurrent requests - connections are reused *)
3543 let (user, repos) = Eio.Fiber.both
3636- (fun () -> Requests.get req "https://api.github.com/user")
3737- (fun () -> Requests.get req "https://api.github.com/user/repos") in
4444+ (fun () -> Requests.get session "https://api.github.com/user")
4545+ (fun () -> Requests.get session "https://api.github.com/user/repos") in
38463947 (* Process responses *)
4040- let user_data = Response.body user |> Eio.Flow.read_all in
4141- let repos_data = Response.body repos |> Eio.Flow.read_all in
4848+ let user_data = Requests.Response.text user in
4949+ let repos_data = Requests.Response.text repos in
5050+ Printf.printf "User: %s\nRepos: %s\n" user_data repos_data
42514343- (* No cleanup needed - responses auto-close with the switch *)
5252+ (* Resources auto-cleanup when switch closes *)
4453 ]}
45544646- {b 2. One-shot requests} (For stateless operations)
5555+ {3 One-Shot API (Requests.One)}
47564848- The One module provides lower-level control for stateless,
4949- one-off requests without session state:
5757+ Use {!Requests.One} for stateless, single requests when you need:
5858+ - {b Minimal overhead} for one-off requests
5959+ - {b Fine-grained control} over TLS and connection settings
6060+ - {b No session state} (no cookies, no connection reuse)
6161+ - {b Direct streaming} without middleware
50625163 {[
5252- (* Create a one-shot client *)
5353- let client = Requests.One.create ~clock:env#clock ~net:env#net () in
6464+ open Eio_main
6565+6666+ let () = run @@ fun env ->
6767+ Eio.Switch.run @@ fun sw ->
6868+6969+ (* Direct stateless request - no session needed *)
7070+ let response = Requests.One.get ~sw
7171+ ~clock:env#clock ~net:env#net
7272+ "https://api.github.com" in
54735555- (* Make a simple GET request *)
5656- let response = Requests.One.get ~sw ~client "https://api.github.com" in
5774 Printf.printf "Status: %d\n" (Requests.Response.status_code response);
58755959- (* POST with custom headers and body *)
6060- let response = Requests.One.post ~sw ~client
7676+ (* POST with JSON body *)
7777+ let json_body = Jsont.Object ([
7878+ ("name", Jsont.String "Alice")
7979+ ], Jsont.Meta.none) in
8080+8181+ let response = Requests.One.post ~sw
8282+ ~clock:env#clock ~net:env#net
6183 ~headers:(Requests.Headers.empty
6262- |> Requests.Headers.content_type Requests.Mime.json
6363- |> Requests.Headers.set "X-API-Key" "secret")
6464- ~body:(Requests.Body.json {|{"name": "Alice"}|})
6565- "https://api.example.com/users"
8484+ |> Requests.Headers.content_type Requests.Mime.json)
8585+ ~body:(Requests.Body.json json_body)
8686+ "https://api.example.com/users" in
66876767- (* No cleanup needed - responses auto-close with the switch *)
8888+ Printf.printf "Created: %d\n" (Requests.Response.status_code response)
6889 ]}
69907091 {2 Features}
···179200 ~sink:(Eio.Path.(fs / "download.zip" |> sink))
180201 ]}
181202182182- {2 Choosing Between Main API and One}
183183-184184- Use the {b main API (Requests.t)} when you need:
185185- - Cookie persistence across requests
186186- - Automatic retry handling
187187- - Shared authentication across requests
188188- - Request/response history tracking
189189- - Configuration persistence to disk
190190-191191- Use {b One} when you need:
192192- - One-off stateless requests
193193- - Fine-grained control over connections
194194- - Minimal overhead
195195- - Custom connection pooling
196196- - Direct streaming without cookies
197203*)
198204199205(** {1 Main API}
···244250 ?xsrf_cookie_name:string option ->
245251 ?xsrf_header_name:string ->
246252 ?proxy:Proxy.config ->
253253+ ?allow_insecure_auth:bool ->
247254 < clock: _ Eio.Time.clock; net: _ Eio.Net.t; fs: Eio.Fs.dir_ty Eio.Path.t; .. > ->
248255 t
249256(** Create a new requests instance with persistent state and connection pooling.
···276283 @param proxy HTTP/HTTPS proxy configuration. When set, requests are routed through the proxy.
277284 HTTP requests use absolute-URI form (RFC 9112 Section 3.2.2).
278285 HTTPS requests use CONNECT tunneling (RFC 9110 Section 9.3.6).
279279-280280- {b Note:} HTTP caching has been disabled for simplicity. See CACHEIO.md for integration notes
281281- if you need to restore caching functionality in the future.
286286+ @param allow_insecure_auth Allow Basic/Bearer/Digest authentication over plaintext HTTP (default: false).
287287+ Per {{:https://datatracker.ietf.org/doc/html/rfc7617#section-4}RFC 7617 Section 4}} and
288288+ {{:https://datatracker.ietf.org/doc/html/rfc6750#section-5.1}RFC 6750 Section 5.1}},
289289+ these auth schemes transmit credentials that SHOULD be protected by TLS.
290290+ {b Only set to [true] for local development or testing environments.}
291291+ Example for local dev server:
292292+ {[
293293+ let session = Requests.create ~sw ~allow_insecure_auth:true env in
294294+ let session = Requests.set_auth session (Requests.Auth.basic ~username:"dev" ~password:"dev") in
295295+ (* Can now make requests to http://localhost:8080 with Basic auth *)
296296+ ]}
282297*)
283298284299(** {2 Configuration Management} *)
···805820806821(** {1 One-Shot API}
807822808808- The One module provides direct control over HTTP requests without
809809- session state. Use this for stateless operations or when you need
810810- fine-grained control.
823823+ The {!One} module provides direct control over HTTP requests without
824824+ session state. Each request opens a new TCP connection that is closed
825825+ when the switch closes.
826826+827827+ Use {!One} for:
828828+ - Single, stateless requests without session overhead
829829+ - Fine-grained control over TLS configuration per request
830830+ - Direct streaming uploads and downloads
831831+ - Situations where connection pooling is not needed
832832+833833+ See the module documentation for examples and full API.
811834*)
812835813813-(** One-shot HTTP client for stateless requests *)
836836+(** One-shot HTTP client for stateless requests.
837837+838838+ Provides {!One.get}, {!One.post}, {!One.put}, {!One.patch}, {!One.delete},
839839+ {!One.head}, {!One.upload}, and {!One.download} functions.
840840+841841+ Example:
842842+ {[
843843+ let response = Requests.One.get ~sw
844844+ ~clock:env#clock ~net:env#net
845845+ "https://example.com" in
846846+ Printf.printf "Status: %d\n" (Requests.Response.status_code response)
847847+ ]} *)
814848module One = One
815849816850(** Low-level HTTP client over pooled connections *)
···878912 Example: [Logs.Src.set_level Requests.src (Some Logs.Debug)] *)
879913val src : Logs.Src.t
880914881881-(** {1 Module-Level Convenience Functions}
882882-883883- These functions perform one-off requests without creating a session.
884884- They are thin wrappers around {!One} module functions with a simplified
885885- environment-based interface.
886886-887887- For multiple requests to the same hosts, prefer creating a session with
888888- {!create} to benefit from connection pooling and cookie persistence.
889889-890890- {b Example:}
891891- {[
892892- Eio_main.run @@ fun env ->
893893- Eio.Switch.run @@ fun sw ->
894894- let response = Requests.simple_get ~sw env "https://example.com" in
895895- Printf.printf "Status: %d\n" (Response.status_code response)
896896- ]}
897897-*)
898898-899899-val simple_get :
900900- sw:Eio.Switch.t ->
901901- < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > ->
902902- ?headers:Headers.t ->
903903- ?auth:Auth.t ->
904904- ?timeout:Timeout.t ->
905905- string ->
906906- Response.t
907907-(** [simple_get ~sw env url] performs a one-off GET request. *)
908908-909909-val simple_post :
910910- sw:Eio.Switch.t ->
911911- < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > ->
912912- ?headers:Headers.t ->
913913- ?body:Body.t ->
914914- ?auth:Auth.t ->
915915- ?timeout:Timeout.t ->
916916- string ->
917917- Response.t
918918-(** [simple_post ~sw env url] performs a one-off POST request. *)
919919-920920-val simple_put :
921921- sw:Eio.Switch.t ->
922922- < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > ->
923923- ?headers:Headers.t ->
924924- ?body:Body.t ->
925925- ?auth:Auth.t ->
926926- ?timeout:Timeout.t ->
927927- string ->
928928- Response.t
929929-(** [simple_put ~sw env url] performs a one-off PUT request. *)
930930-931931-val simple_patch :
932932- sw:Eio.Switch.t ->
933933- < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > ->
934934- ?headers:Headers.t ->
935935- ?body:Body.t ->
936936- ?auth:Auth.t ->
937937- ?timeout:Timeout.t ->
938938- string ->
939939- Response.t
940940-(** [simple_patch ~sw env url] performs a one-off PATCH request. *)
941941-942942-val simple_delete :
943943- sw:Eio.Switch.t ->
944944- < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > ->
945945- ?headers:Headers.t ->
946946- ?auth:Auth.t ->
947947- ?timeout:Timeout.t ->
948948- string ->
949949- Response.t
950950-(** [simple_delete ~sw env url] performs a one-off DELETE request. *)
951951-952952-val simple_head :
953953- sw:Eio.Switch.t ->
954954- < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > ->
955955- ?headers:Headers.t ->
956956- ?auth:Auth.t ->
957957- ?timeout:Timeout.t ->
958958- string ->
959959- Response.t
960960-(** [simple_head ~sw env url] performs a one-off HEAD request. *)
+3-1
lib/response.mli
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** HTTP response handling
66+(** HTTP response handling per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-15}RFC 9110}
7788 This module represents HTTP responses and provides functions to access
99 status codes, headers, and response bodies. Responses support streaming
1010 to efficiently handle large payloads.
1111+1212+ Caching semantics follow {{:https://datatracker.ietf.org/doc/html/rfc9111}RFC 9111}} (HTTP Caching).
11131214 {2 Examples}
1315
+7-2
lib/retry.mli
···7788 This module provides configurable retry logic for HTTP requests,
99 including exponential backoff, custom retry predicates, and
1010- Retry-After header support.
1010+ Retry-After header support per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-10.2.3}RFC 9110 Section 10.2.3}}.
11111212 {2 Custom Retry Predicates}
1313···105105val calculate_backoff : config:config -> attempt:int -> float
106106107107(** Parse Retry-After header value (seconds or HTTP date).
108108- Per RFC 9110 Section 10.2.3 and Recommendation #5:
108108+109109+ Per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-10.2.3}RFC 9110 Section 10.2.3}},
110110+ Retry-After can be either:
111111+ - A non-negative integer (delay in seconds)
112112+ - An HTTP-date (absolute time to retry after)
113113+109114 Values are capped to [backoff_max] (default 120s) to prevent DoS
110115 from malicious servers specifying extremely long delays. *)
111116val parse_retry_after : ?backoff_max:float -> string -> float option
+13-1
lib/status.mli
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** HTTP status codes following RFC 7231 and extensions *)
66+(** HTTP status codes per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-15}RFC 9110 Section 15}
77+88+ This module provides types and functions for working with HTTP response
99+ status codes. Status codes are three-digit integers that indicate the
1010+ result of an HTTP request.
1111+1212+ {2 Status Code Classes}
1313+1414+ - {b 1xx Informational}: Request received, continuing process
1515+ - {b 2xx Success}: Request successfully received, understood, and accepted
1616+ - {b 3xx Redirection}: Further action needed to complete the request
1717+ - {b 4xx Client Error}: Request contains bad syntax or cannot be fulfilled
1818+ - {b 5xx Server Error}: Server failed to fulfill a valid request *)
719820(** Log source for status code operations *)
921val src : Logs.Src.t
···5566(** Comprehensive tests for HTTP-date parsing per RFC 9110 Section 5.6.7 *)
7788-open Requests
88+let parse_http_date = Requests.Response.parse_http_date
991010(** Alcotest testable for Ptime.t *)
1111module Alcotest_ptime = struct
···25252626let test_rfc1123_basic () =
2727 (* RFC 9110 Section 5.6.7: preferred format "Sun, 06 Nov 1994 08:49:37 GMT" *)
2828- let result = Http_date.parse "Sun, 06 Nov 1994 08:49:37 GMT" in
2828+ let result = parse_http_date "Sun, 06 Nov 1994 08:49:37 GMT" in
2929 let expected = Some (make_time 1994 11 6 8 49 37) in
3030 Alcotest.(check (option Alcotest_ptime.testable))
3131 "RFC 1123 basic parsing" expected result
···3939 ] in
4040 List.iter (fun (month_str, month_num) ->
4141 let date_str = Printf.sprintf "Mon, 01 %s 2020 00:00:00 GMT" month_str in
4242- let result = Http_date.parse date_str in
4242+ let result = parse_http_date date_str in
4343 let expected = Some (make_time 2020 month_num 1 0 0 0) in
4444 Alcotest.(check (option Alcotest_ptime.testable))
4545 (Printf.sprintf "RFC 1123 month %s" month_str) expected result
···5050 let weekdays = ["Sun"; "Mon"; "Tue"; "Wed"; "Thu"; "Fri"; "Sat"] in
5151 List.iter (fun wday ->
5252 let date_str = Printf.sprintf "%s, 06 Nov 1994 08:49:37 GMT" wday in
5353- let result = Http_date.parse date_str in
5353+ let result = parse_http_date date_str in
5454 let expected = Some (make_time 1994 11 6 8 49 37) in
5555 Alcotest.(check (option Alcotest_ptime.testable))
5656 (Printf.sprintf "RFC 1123 weekday %s" wday) expected result
···6666 ("Fri, 13 Dec 2024 23:59:59 GMT", 2024, 12, 13, 23, 59, 59, "Near current");
6767 ] in
6868 List.iter (fun (date_str, y, m, d, h, min, s, desc) ->
6969- let result = Http_date.parse date_str in
6969+ let result = parse_http_date date_str in
7070 let expected = Some (make_time y m d h min s) in
7171 Alcotest.(check (option Alcotest_ptime.testable))
7272 (Printf.sprintf "RFC 1123 edge: %s" desc) expected result
···76767777let test_rfc850_basic () =
7878 (* RFC 850 format: "Sunday, 06-Nov-94 08:49:37 GMT" *)
7979- let result = Http_date.parse "Sunday, 06-Nov-94 08:49:37 GMT" in
7979+ let result = parse_http_date "Sunday, 06-Nov-94 08:49:37 GMT" in
8080 let expected = Some (make_time 1994 11 6 8 49 37) in
8181 Alcotest.(check (option Alcotest_ptime.testable))
8282 "RFC 850 basic parsing (2-digit year)" expected result
···9191 ("Thursday, 01-Jan-69 00:00:00 GMT", 2069, "Year 69 -> 2069");
9292 ] in
9393 List.iter (fun (date_str, expected_year, desc) ->
9494- let result = Http_date.parse date_str in
9494+ let result = parse_http_date date_str in
9595 let expected = Some (make_time expected_year 1 1 0 0 0) in
9696 Alcotest.(check (option Alcotest_ptime.testable))
9797 (Printf.sprintf "RFC 850 %s" desc) expected result
···101101102102let test_asctime_basic () =
103103 (* asctime() format: "Sun Nov 6 08:49:37 1994" *)
104104- let result = Http_date.parse "Sun Nov 6 08:49:37 1994" in
104104+ let result = parse_http_date "Sun Nov 6 08:49:37 1994" in
105105 let expected = Some (make_time 1994 11 6 8 49 37) in
106106 Alcotest.(check (option Alcotest_ptime.testable))
107107 "asctime basic parsing" expected result
···113113 ("Sun Nov 9 08:49:37 1994", 9, "Day 9");
114114 ] in
115115 List.iter (fun (date_str, day, desc) ->
116116- let result = Http_date.parse date_str in
116116+ let result = parse_http_date date_str in
117117 let expected = Some (make_time 1994 11 day 8 49 37) in
118118 Alcotest.(check (option Alcotest_ptime.testable))
119119 (Printf.sprintf "asctime %s" desc) expected result
···131131 "13-Dec-2024"; (* No day name *)
132132 ] in
133133 List.iter (fun input ->
134134- let result = Http_date.parse input in
134134+ let result = parse_http_date input in
135135 Alcotest.(check (option Alcotest_ptime.testable))
136136 (Printf.sprintf "Invalid input: %S" input) None result
137137 ) invalid_inputs
···144144 "Sun, 06 November 1994 08:49:37 GMT"; (* Full month name *)
145145 ] in
146146 List.iter (fun input ->
147147- let result = Http_date.parse input in
147147+ let result = parse_http_date input in
148148 Alcotest.(check (option Alcotest_ptime.testable))
149149 (Printf.sprintf "Invalid month: %S" input) None result
150150 ) invalid_months
···158158 "Sun, 31 Apr 2020 00:00:00 GMT"; (* April has 30 days *)
159159 ] in
160160 List.iter (fun input ->
161161- let result = Http_date.parse input in
161161+ let result = parse_http_date input in
162162 Alcotest.(check (option Alcotest_ptime.testable))
163163 (Printf.sprintf "Invalid date: %S" input) None result
164164 ) invalid_dates
···171171 "Sun, 06 Nov 1994 00:00:60 GMT"; (* Second 60 (no leap second support) *)
172172 ] in
173173 List.iter (fun input ->
174174- let result = Http_date.parse input in
174174+ let result = parse_http_date input in
175175 Alcotest.(check (option Alcotest_ptime.testable))
176176 (Printf.sprintf "Invalid time: %S" input) None result
177177 ) invalid_times
···187187 ] in
188188 let expected = Some (make_time 1994 11 6 8 49 37) in
189189 List.iter (fun input ->
190190- let result = Http_date.parse input in
190190+ let result = parse_http_date input in
191191 Alcotest.(check (option Alcotest_ptime.testable))
192192 "Whitespace trimming" expected result
193193 ) test_cases
···201201 ] in
202202 let expected = Some (make_time 1994 11 6 8 49 37) in
203203 List.iter (fun (input, desc) ->
204204- let result = Http_date.parse input in
204204+ let result = parse_http_date input in
205205 Alcotest.(check (option Alcotest_ptime.testable))
206206 (Printf.sprintf "Case insensitive: %s" desc) expected result
207207 ) test_cases
+3-2
test/test_one.ml
···11(* Test using One module directly without connection pooling *)
22let () =
33- Eio_main.run @@ fun env ->
43 Mirage_crypto_rng_unix.use_default ();
44+ Eio_main.run @@ fun env ->
55 Eio.Switch.run @@ fun sw ->
66 try
77- let response = Requests.simple_get ~sw env "https://opam.ocaml.org" in
77+ let response = Requests.One.get ~sw ~clock:env#clock ~net:env#net
88+ "https://opam.ocaml.org" in
89 Printf.printf "Status: %d\n%!" (Requests.Response.status_code response)
910 with e ->
1011 Printf.printf "Exception: %s\n%!" (Printexc.to_string e);