A batteries included HTTP/1.1 client in OCaml

tests

+429 -251
+9 -5
bin/ocurl.ml
··· 53 53 let doc = "Basic authentication in USER:PASSWORD format" in 54 54 Arg.(value & opt (some string) None & info ["u"; "user"] ~docv:"USER:PASS" ~doc) 55 55 56 + let allow_insecure_auth = 57 + let doc = "Allow basic authentication over HTTP (insecure, for testing only)" in 58 + Arg.(value & flag & info ["allow-insecure-auth"] ~doc) 59 + 56 60 let show_progress = 57 61 let doc = "Show progress bar for downloads" in 58 62 Arg.(value & flag & info ["progress-bar"] ~doc) ··· 211 215 (* Main function using Requests with concurrent fetching *) 212 216 let run_request env sw persist_cookies verify_tls timeout follow_redirects max_redirects 213 217 method_ urls headers data json_data output include_headers head 214 - auth _show_progress () = 218 + auth allow_insecure_auth _show_progress () = 215 219 216 220 (* Log levels are already set by setup_log via Logs_cli *) 217 221 ··· 221 225 (* Create requests instance with configuration *) 222 226 let timeout_obj = Option.map (fun t -> Requests.Timeout.create ~total:t ()) timeout in 223 227 let req = Requests.create ~sw ~xdg ~persist_cookies ~verify_tls 224 - ~follow_redirects ~max_redirects ?timeout:timeout_obj env in 228 + ~follow_redirects ~max_redirects ~allow_insecure_auth ?timeout:timeout_obj env in 225 229 226 230 (* Set authentication if provided *) 227 231 let req = match auth with ··· 301 305 302 306 (* Main entry point *) 303 307 let main method_ urls headers data json_data output include_headers head 304 - auth show_progress persist_cookies_ws verify_tls_ws 308 + auth allow_insecure_auth show_progress persist_cookies_ws verify_tls_ws 305 309 timeout_ws follow_redirects_ws max_redirects_ws () = 306 310 307 311 (* Extract values from with_source wrappers *) ··· 317 321 318 322 run_request env sw persist_cookies verify_tls timeout follow_redirects max_redirects 319 323 method_ urls headers data json_data output include_headers head auth 320 - show_progress () 324 + allow_insecure_auth show_progress () 321 325 322 326 (* Command-line interface *) 323 327 let cmd = ··· 377 381 let combined_term = 378 382 Term.(const main $ http_method $ urls $ headers $ data $ json_data $ 379 383 output_file $ include_headers $ head $ auth $ 380 - show_progress $ 384 + allow_insecure_auth $ show_progress $ 381 385 Requests.Cmd.persist_cookies_term app_name $ 382 386 Requests.Cmd.verify_tls_term app_name $ 383 387 Requests.Cmd.timeout_term app_name $
+14 -10
examples/session_example.ml
··· 6 6 open Eio 7 7 8 8 let () = 9 + Mirage_crypto_rng_unix.use_default (); 9 10 Eio_main.run @@ fun env -> 10 - Mirage_crypto_rng_eio.run (module Mirage_crypto_rng.Fortuna) env @@ fun () -> 11 11 Switch.run @@ fun sw -> 12 12 13 13 (* Example 1: Basic GET request *) ··· 111 111 (* Example 10: Error handling *) 112 112 Printf.printf "\n=== Example 10: Error Handling ===\n%!"; 113 113 (try 114 - let _resp = Requests.get req "https://httpbin.org/status/404" in 115 - Printf.printf "Got 404 response (no exception thrown)\n%!" 114 + let resp = Requests.get req "https://httpbin.org/status/404" in 115 + (* By default, 4xx/5xx status codes don't raise exceptions *) 116 + if Requests.Response.ok resp then 117 + Printf.printf "Success\n%!" 118 + else 119 + Printf.printf "Got %d response (no exception by default)\n%!" 120 + (Requests.Response.status_code resp); 121 + (* Use raise_for_status to get exception behavior *) 122 + let _resp = Requests.Response.raise_for_status resp in 123 + () 116 124 with 117 - | Requests.Error.HTTPError { status; url; _ } -> 118 - Printf.printf "HTTP Error: %d for %s\n%!" status url 119 - | Requests.Error.Timeout -> 120 - Printf.printf "Request timed out\n%!" 121 - | Requests.Error.ConnectionError msg -> 122 - Printf.printf "Connection error: %s\n%!" msg 125 + | Eio.Io (Requests.Error.E (Requests.Error.Http_error_status _), _) -> 126 + Printf.printf "HTTP error status raised via raise_for_status\n%!" 123 127 | exn -> 124 128 Printf.printf "Other error: %s\n%!" (Printexc.to_string exn)); 125 129 ··· 133 137 let resp11 = Requests.get req_timeout "https://httpbin.org/delay/2" in 134 138 Printf.printf "Timeout test completed: %d\n%!" (Requests.Response.status_code resp11) 135 139 with 136 - | Requests.Error.Timeout -> 140 + | Eio.Time.Timeout -> 137 141 Printf.printf "Request correctly timed out\n%!" 138 142 | exn -> 139 143 Printf.printf "Other timeout error: %s\n%!" (Printexc.to_string exn));
+14 -1
lib/auth.mli
··· 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 - (** Authentication mechanisms *) 6 + (** HTTP authentication mechanisms 7 + 8 + This module provides authentication schemes for HTTP requests: 9 + 10 + - {b Basic}: {{:https://datatracker.ietf.org/doc/html/rfc7617}RFC 7617} - Base64 username:password 11 + - {b Bearer}: {{:https://datatracker.ietf.org/doc/html/rfc6750}RFC 6750} - OAuth 2.0 tokens 12 + - {b Digest}: {{:https://datatracker.ietf.org/doc/html/rfc7616}RFC 7616} - Challenge-response with MD5/SHA-256 13 + 14 + {2 Security} 15 + 16 + Per {{:https://datatracker.ietf.org/doc/html/rfc7617#section-4}RFC 7617 Section 4}} and 17 + {{:https://datatracker.ietf.org/doc/html/rfc6750#section-5.1}RFC 6750 Section 5.1}}, 18 + Basic, Bearer, and Digest authentication transmit credentials that MUST be 19 + protected by TLS. The library enforces HTTPS by default for these schemes. *) 7 20 8 21 (** Log source for authentication operations *) 9 22 val src : Logs.Src.t
+9 -3
lib/headers.mli
··· 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 - (** HTTP headers management with case-insensitive keys 6 + (** HTTP header field handling per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-5}RFC 9110 Section 5} 7 7 8 8 This module provides an efficient implementation of HTTP headers with 9 - case-insensitive header names as per RFC 7230. Headers can have multiple 10 - values for the same key (e.g., multiple Set-Cookie headers). 9 + case-insensitive field names per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-5.1}RFC 9110 Section 5.1}}. 10 + Headers can have multiple values for the same field name (e.g., Set-Cookie). 11 + 12 + {2 Security} 13 + 14 + Header names and values are validated to prevent HTTP header injection 15 + attacks. CR and LF characters are rejected per 16 + {{:https://datatracker.ietf.org/doc/html/rfc9110#section-5.5}RFC 9110 Section 5.5}}. 11 17 12 18 {2 Examples} 13 19
+3 -3
lib/http_client.ml
··· 177 177 178 178 (* Read response using Buf_read *) 179 179 let buf_read = Http_read.of_flow flow ~max_size:max_int in 180 - let (_version, status, headers, body) = Http_read.response ~limits buf_read in 180 + let (_version, status, headers, body) = Http_read.response ~limits ~method_ buf_read in 181 181 (status, headers, body) 182 182 183 183 (** Make HTTP request with optional auto-decompression *) ··· 336 336 337 337 (* Read final response *) 338 338 let buf_read = Http_read.of_flow flow ~max_size:max_int in 339 - let (_version, status, headers, body) = Http_read.response ~limits buf_read in 339 + let (_version, status, headers, body) = Http_read.response ~limits ~method_ buf_read in 340 340 (status, headers, body) 341 341 342 342 | Rejected (status, resp_headers, resp_body_str) -> ··· 361 361 362 362 (* Read response *) 363 363 let buf_read = Http_read.of_flow flow ~max_size:max_int in 364 - let (_version, status, headers, body) = Http_read.response ~limits buf_read in 364 + let (_version, status, headers, body) = Http_read.response ~limits ~method_ buf_read in 365 365 (status, headers, body) 366 366 end 367 367
+45 -27
lib/http_read.ml
··· 376 376 377 377 (** {1 High-level Response Parsing} *) 378 378 379 + (** Check if response should have no body per RFC 9110. 380 + Per RFC 9110 Section 6.4.1: 381 + - Any response to a HEAD request 382 + - Any 1xx (Informational) response 383 + - 204 (No Content) response 384 + - 304 (Not Modified) response *) 385 + let response_has_no_body ~method_ ~status = 386 + (method_ = Some `HEAD) || 387 + (status >= 100 && status < 200) || 388 + status = 204 || 389 + status = 304 390 + 379 391 (** Parse complete response (status + headers + body) to string *) 380 - let response ~limits r = 392 + let response ~limits ?method_ r = 381 393 let (version, status) = status_line r in 382 394 let hdrs = headers ~limits r in 383 395 384 - (* Determine how to read body *) 385 - let transfer_encoding = Headers.get "transfer-encoding" hdrs in 386 - let content_length = Headers.get "content-length" hdrs |> Option.map Int64.of_string in 396 + (* Per RFC 9110 Section 6.4.1: Certain responses MUST NOT have a body *) 397 + if response_has_no_body ~method_ ~status then begin 398 + Log.debug (fun m -> m "Response has no body (HEAD, 1xx, 204, or 304)"); 399 + (version, status, hdrs, "") 400 + end else begin 401 + (* Determine how to read body *) 402 + let transfer_encoding = Headers.get "transfer-encoding" hdrs in 403 + let content_length = Headers.get "content-length" hdrs |> Option.map Int64.of_string in 387 404 388 - (* Per RFC 9112 Section 6.3: When both Transfer-Encoding and Content-Length 389 - are present, Transfer-Encoding takes precedence. The presence of both 390 - headers is a potential HTTP request smuggling attack indicator. *) 391 - let body = match transfer_encoding, content_length with 392 - | Some te, Some _ when String.lowercase_ascii te |> String.trim = "chunked" -> 393 - (* Both headers present - log warning per RFC 9112 Section 6.3 *) 394 - Log.warn (fun m -> m "Both Transfer-Encoding and Content-Length present - \ 395 - ignoring Content-Length per RFC 9112 (potential attack indicator)"); 396 - chunked_body ~limits r 397 - | Some te, None when String.lowercase_ascii te |> String.trim = "chunked" -> 398 - Log.debug (fun m -> m "Reading chunked response body"); 399 - chunked_body ~limits r 400 - | _, Some len -> 401 - Log.debug (fun m -> m "Reading fixed-length response body (%Ld bytes)" len); 402 - fixed_body ~limits ~length:len r 403 - | Some other_te, None -> 404 - Log.warn (fun m -> m "Unsupported transfer-encoding: %s, assuming no body" other_te); 405 - "" 406 - | None, None -> 407 - Log.debug (fun m -> m "No body indicated"); 408 - "" 409 - in 405 + (* Per RFC 9112 Section 6.3: When both Transfer-Encoding and Content-Length 406 + are present, Transfer-Encoding takes precedence. The presence of both 407 + headers is a potential HTTP request smuggling attack indicator. *) 408 + let body = match transfer_encoding, content_length with 409 + | Some te, Some _ when String.lowercase_ascii te |> String.trim = "chunked" -> 410 + (* Both headers present - log warning per RFC 9112 Section 6.3 *) 411 + Log.warn (fun m -> m "Both Transfer-Encoding and Content-Length present - \ 412 + ignoring Content-Length per RFC 9112 (potential attack indicator)"); 413 + chunked_body ~limits r 414 + | Some te, None when String.lowercase_ascii te |> String.trim = "chunked" -> 415 + Log.debug (fun m -> m "Reading chunked response body"); 416 + chunked_body ~limits r 417 + | _, Some len -> 418 + Log.debug (fun m -> m "Reading fixed-length response body (%Ld bytes)" len); 419 + fixed_body ~limits ~length:len r 420 + | Some other_te, None -> 421 + Log.warn (fun m -> m "Unsupported transfer-encoding: %s, assuming no body" other_te); 422 + "" 423 + | None, None -> 424 + Log.debug (fun m -> m "No body indicated"); 425 + "" 426 + in 410 427 411 - (version, status, hdrs, body) 428 + (version, status, hdrs, body) 429 + end 412 430 413 431 (** Response with streaming body *) 414 432 type stream_response = {
+6 -2
lib/http_read.mli
··· 88 88 89 89 (** {1 High-level Response Parsing} *) 90 90 91 - val response : limits:limits -> Eio.Buf_read.t -> http_version * int * Headers.t * string 92 - (** [response ~limits r] parses a complete HTTP response including: 91 + val response : limits:limits -> ?method_:Method.t -> Eio.Buf_read.t -> http_version * int * Headers.t * string 92 + (** [response ~limits ?method_ r] parses a complete HTTP response including: 93 93 - HTTP version 94 94 - Status code 95 95 - Headers 96 96 - Body (based on Transfer-Encoding or Content-Length) 97 97 98 98 Returns [(http_version, status, headers, body)]. 99 + 100 + @param method_ The HTTP method of the request. When [`HEAD], the body 101 + is always empty regardless of Content-Length header (per RFC 9110 102 + Section 9.3.2). Similarly for 1xx, 204, and 304 responses. 99 103 100 104 This reads the entire body into memory. For large responses, 101 105 use {!response_stream} instead. *)
+15 -1
lib/method.mli
··· 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 - (** HTTP methods following RFC 7231 *) 6 + (** HTTP request methods per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-9}RFC 9110 Section 9} 7 + 8 + HTTP methods indicate the desired action to be performed on a resource. 9 + The method token is case-sensitive. 10 + 11 + {2 Safe Methods} 12 + 13 + Methods are considered "safe" if their semantics are read-only (GET, HEAD, 14 + OPTIONS, TRACE). Per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-9.2.1}RFC 9110 Section 9.2.1}}. 15 + 16 + {2 Idempotent Methods} 17 + 18 + A method is "idempotent" if multiple identical requests have the same effect 19 + as a single request (GET, HEAD, PUT, DELETE, OPTIONS, TRACE). 20 + Per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-9.2.2}RFC 9110 Section 9.2.2}. *) 7 21 8 22 (** Log source for method operations *) 9 23 val src : Logs.Src.t
+1 -1
lib/one.ml
··· 266 266 let limits = Response_limits.default in 267 267 let buf_read = Http_read.of_flow ~max_size:65536 flow in 268 268 let (_version, status, resp_headers, body_str) = 269 - Http_read.response ~limits buf_read in 269 + Http_read.response ~limits ~method_ buf_read in 270 270 (* Handle decompression if enabled *) 271 271 let body_str = 272 272 if auto_decompress then
+6 -2
lib/one.mli
··· 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 - (** One-shot HTTP client for stateless requests 6 + (** One-shot HTTP/1.1 client for stateless requests 7 7 8 8 The One module provides a stateless HTTP client for single requests without 9 9 session state like cookies, connection pooling, or persistent configuration. 10 - Each request opens a new connection that is closed after use. 10 + Each request opens a new TCP connection (with TLS for https://) that is 11 + closed when the Eio switch closes. 12 + 13 + Implements {{:https://datatracker.ietf.org/doc/html/rfc9112}RFC 9112}} (HTTP/1.1) 14 + with {{:https://datatracker.ietf.org/doc/html/rfc9110}RFC 9110}} semantics. 11 15 12 16 For stateful requests with automatic cookie handling, connection pooling, 13 17 and persistent configuration, use the main {!Requests} module instead.
+6 -27
lib/requests.ml
··· 71 71 xsrf_cookie_name : string option; (** Per Recommendation #24: XSRF cookie name *) 72 72 xsrf_header_name : string; (** Per Recommendation #24: XSRF header name *) 73 73 proxy : Proxy.config option; (** HTTP/HTTPS proxy configuration *) 74 + allow_insecure_auth : bool; (** Allow auth over HTTP for dev/testing *) 74 75 75 76 (* Statistics - mutable but NOTE: when sessions are derived via record update 76 77 syntax ({t with field = value}), these are copied not shared. Each derived ··· 107 108 ?(xsrf_cookie_name = Some "XSRF-TOKEN") (* Per Recommendation #24 *) 108 109 ?(xsrf_header_name = "X-XSRF-TOKEN") 109 110 ?proxy 111 + ?(allow_insecure_auth = false) 110 112 env = 111 113 112 114 let clock = env#clock in ··· 231 233 xsrf_cookie_name; 232 234 xsrf_header_name; 233 235 proxy; 236 + allow_insecure_auth; 234 237 requests_made = 0; 235 238 total_time = 0.0; 236 239 retries_count = 0; ··· 450 453 | None -> t.auth 451 454 in 452 455 453 - (* Apply auth *) 456 + (* Apply auth with HTTPS validation per RFC 7617/6750 *) 454 457 let headers = match auth with 455 458 | Some a -> 456 459 Log.debug (fun m -> m "Applying authentication"); 457 - Auth.apply a headers 460 + Auth.apply_secure ~allow_insecure_auth:t.allow_insecure_auth ~url a headers 458 461 | None -> headers 459 462 in 460 463 ··· 602 605 let limits = Response_limits.default in 603 606 let buf_read = Http_read.of_flow ~max_size:65536 flow in 604 607 let (_version, status, resp_headers, body_str) = 605 - Http_read.response ~limits buf_read in 608 + Http_read.response ~limits ~method_ buf_read in 606 609 (* Handle decompression if enabled *) 607 610 let body_str = 608 611 if t.auto_decompress then ··· 1368 1371 set_tls_tracing_level Logs.Warning 1369 1372 end 1370 1373 1371 - (** {1 Module-Level Convenience Functions} 1372 - 1373 - These functions perform one-off requests without creating a session. 1374 - They are thin wrappers around {!One} module functions. 1375 - For multiple requests to the same hosts, prefer creating a session with {!create} 1376 - to benefit from connection pooling and cookie persistence. *) 1377 - 1378 - let simple_get ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?auth ?timeout url = 1379 - One.get ~sw ~clock:env#clock ~net:env#net ?headers ?auth ?timeout url 1380 - 1381 - let simple_post ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?body ?auth ?timeout url = 1382 - One.post ~sw ~clock:env#clock ~net:env#net ?headers ?body ?auth ?timeout url 1383 - 1384 - let simple_put ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?body ?auth ?timeout url = 1385 - One.put ~sw ~clock:env#clock ~net:env#net ?headers ?body ?auth ?timeout url 1386 - 1387 - let simple_patch ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?body ?auth ?timeout url = 1388 - One.patch ~sw ~clock:env#clock ~net:env#net ?headers ?body ?auth ?timeout url 1389 - 1390 - let simple_delete ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?auth ?timeout url = 1391 - One.delete ~sw ~clock:env#clock ~net:env#net ?headers ?auth ?timeout url 1392 - 1393 - let simple_head ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?auth ?timeout url = 1394 - One.head ~sw ~clock:env#clock ~net:env#net ?headers ?auth ?timeout url
+87 -133
lib/requests.mli
··· 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 - (** Requests - A modern HTTP client library for OCaml 6 + (** Requests - A modern HTTP/1.1 client library for OCaml 7 7 8 8 Requests is an HTTP client library for OCaml inspired by Python's requests 9 9 and urllib3 libraries. It provides a simple, intuitive API for making HTTP 10 10 requests while handling complexities like TLS configuration, connection 11 11 pooling, retries, and cookie management. 12 12 13 - {2 High-Level API} 13 + The library implements {{:https://datatracker.ietf.org/doc/html/rfc9110}RFC 9110} 14 + (HTTP Semantics) and {{:https://datatracker.ietf.org/doc/html/rfc9112}RFC 9112} 15 + (HTTP/1.1). 16 + 17 + {2 Choosing an API} 14 18 15 - The Requests library offers two main ways to make HTTP requests: 19 + The library offers two APIs optimized for different use cases: 16 20 17 - {b 1. Main API} (Recommended for most use cases) 21 + {3 Session API (Requests.t)} 18 22 19 - The main API maintains state across requests, handles cookies automatically, 20 - spawns requests in concurrent fibers, and provides a simple interface: 23 + Use {!Requests.create} when you need: 24 + - {b Cookie persistence} across requests (automatic session handling) 25 + - {b Connection pooling} for efficient reuse of TCP/TLS connections 26 + - {b Shared authentication} configured once for all requests 27 + - {b Automatic retry handling} with exponential backoff 28 + - {b Base URL support} for API clients ({{:https://datatracker.ietf.org/doc/html/rfc3986#section-5}RFC 3986 Section 5}) 21 29 22 30 {[ 23 31 open Eio_main 24 32 25 33 let () = run @@ fun env -> 26 - Switch.run @@ fun sw -> 34 + Eio.Switch.run @@ fun sw -> 27 35 28 - (* Create a requests instance *) 29 - let req = Requests.create ~sw env in 36 + (* Create a session with connection pooling *) 37 + let session = Requests.create ~sw env in 30 38 31 - (* Configure authentication once *) 32 - Requests.set_auth req (Requests.Auth.bearer "your-token"); 39 + (* Configure authentication once for all requests *) 40 + let session = Requests.set_auth session (Requests.Auth.bearer "your-token") in 33 41 34 - (* Make concurrent requests using Fiber.both *) 42 + (* Make concurrent requests - connections are reused *) 35 43 let (user, repos) = Eio.Fiber.both 36 - (fun () -> Requests.get req "https://api.github.com/user") 37 - (fun () -> Requests.get req "https://api.github.com/user/repos") in 44 + (fun () -> Requests.get session "https://api.github.com/user") 45 + (fun () -> Requests.get session "https://api.github.com/user/repos") in 38 46 39 47 (* Process responses *) 40 - let user_data = Response.body user |> Eio.Flow.read_all in 41 - let repos_data = Response.body repos |> Eio.Flow.read_all in 48 + let user_data = Requests.Response.text user in 49 + let repos_data = Requests.Response.text repos in 50 + Printf.printf "User: %s\nRepos: %s\n" user_data repos_data 42 51 43 - (* No cleanup needed - responses auto-close with the switch *) 52 + (* Resources auto-cleanup when switch closes *) 44 53 ]} 45 54 46 - {b 2. One-shot requests} (For stateless operations) 55 + {3 One-Shot API (Requests.One)} 47 56 48 - The One module provides lower-level control for stateless, 49 - one-off requests without session state: 57 + Use {!Requests.One} for stateless, single requests when you need: 58 + - {b Minimal overhead} for one-off requests 59 + - {b Fine-grained control} over TLS and connection settings 60 + - {b No session state} (no cookies, no connection reuse) 61 + - {b Direct streaming} without middleware 50 62 51 63 {[ 52 - (* Create a one-shot client *) 53 - let client = Requests.One.create ~clock:env#clock ~net:env#net () in 64 + open Eio_main 65 + 66 + let () = run @@ fun env -> 67 + Eio.Switch.run @@ fun sw -> 68 + 69 + (* Direct stateless request - no session needed *) 70 + let response = Requests.One.get ~sw 71 + ~clock:env#clock ~net:env#net 72 + "https://api.github.com" in 54 73 55 - (* Make a simple GET request *) 56 - let response = Requests.One.get ~sw ~client "https://api.github.com" in 57 74 Printf.printf "Status: %d\n" (Requests.Response.status_code response); 58 75 59 - (* POST with custom headers and body *) 60 - let response = Requests.One.post ~sw ~client 76 + (* POST with JSON body *) 77 + let json_body = Jsont.Object ([ 78 + ("name", Jsont.String "Alice") 79 + ], Jsont.Meta.none) in 80 + 81 + let response = Requests.One.post ~sw 82 + ~clock:env#clock ~net:env#net 61 83 ~headers:(Requests.Headers.empty 62 - |> Requests.Headers.content_type Requests.Mime.json 63 - |> Requests.Headers.set "X-API-Key" "secret") 64 - ~body:(Requests.Body.json {|{"name": "Alice"}|}) 65 - "https://api.example.com/users" 84 + |> Requests.Headers.content_type Requests.Mime.json) 85 + ~body:(Requests.Body.json json_body) 86 + "https://api.example.com/users" in 66 87 67 - (* No cleanup needed - responses auto-close with the switch *) 88 + Printf.printf "Created: %d\n" (Requests.Response.status_code response) 68 89 ]} 69 90 70 91 {2 Features} ··· 179 200 ~sink:(Eio.Path.(fs / "download.zip" |> sink)) 180 201 ]} 181 202 182 - {2 Choosing Between Main API and One} 183 - 184 - Use the {b main API (Requests.t)} when you need: 185 - - Cookie persistence across requests 186 - - Automatic retry handling 187 - - Shared authentication across requests 188 - - Request/response history tracking 189 - - Configuration persistence to disk 190 - 191 - Use {b One} when you need: 192 - - One-off stateless requests 193 - - Fine-grained control over connections 194 - - Minimal overhead 195 - - Custom connection pooling 196 - - Direct streaming without cookies 197 203 *) 198 204 199 205 (** {1 Main API} ··· 244 250 ?xsrf_cookie_name:string option -> 245 251 ?xsrf_header_name:string -> 246 252 ?proxy:Proxy.config -> 253 + ?allow_insecure_auth:bool -> 247 254 < clock: _ Eio.Time.clock; net: _ Eio.Net.t; fs: Eio.Fs.dir_ty Eio.Path.t; .. > -> 248 255 t 249 256 (** Create a new requests instance with persistent state and connection pooling. ··· 276 283 @param proxy HTTP/HTTPS proxy configuration. When set, requests are routed through the proxy. 277 284 HTTP requests use absolute-URI form (RFC 9112 Section 3.2.2). 278 285 HTTPS requests use CONNECT tunneling (RFC 9110 Section 9.3.6). 279 - 280 - {b Note:} HTTP caching has been disabled for simplicity. See CACHEIO.md for integration notes 281 - if you need to restore caching functionality in the future. 286 + @param allow_insecure_auth Allow Basic/Bearer/Digest authentication over plaintext HTTP (default: false). 287 + Per {{:https://datatracker.ietf.org/doc/html/rfc7617#section-4}RFC 7617 Section 4}} and 288 + {{:https://datatracker.ietf.org/doc/html/rfc6750#section-5.1}RFC 6750 Section 5.1}}, 289 + these auth schemes transmit credentials that SHOULD be protected by TLS. 290 + {b Only set to [true] for local development or testing environments.} 291 + Example for local dev server: 292 + {[ 293 + let session = Requests.create ~sw ~allow_insecure_auth:true env in 294 + let session = Requests.set_auth session (Requests.Auth.basic ~username:"dev" ~password:"dev") in 295 + (* Can now make requests to http://localhost:8080 with Basic auth *) 296 + ]} 282 297 *) 283 298 284 299 (** {2 Configuration Management} *) ··· 805 820 806 821 (** {1 One-Shot API} 807 822 808 - The One module provides direct control over HTTP requests without 809 - session state. Use this for stateless operations or when you need 810 - fine-grained control. 823 + The {!One} module provides direct control over HTTP requests without 824 + session state. Each request opens a new TCP connection that is closed 825 + when the switch closes. 826 + 827 + Use {!One} for: 828 + - Single, stateless requests without session overhead 829 + - Fine-grained control over TLS configuration per request 830 + - Direct streaming uploads and downloads 831 + - Situations where connection pooling is not needed 832 + 833 + See the module documentation for examples and full API. 811 834 *) 812 835 813 - (** One-shot HTTP client for stateless requests *) 836 + (** One-shot HTTP client for stateless requests. 837 + 838 + Provides {!One.get}, {!One.post}, {!One.put}, {!One.patch}, {!One.delete}, 839 + {!One.head}, {!One.upload}, and {!One.download} functions. 840 + 841 + Example: 842 + {[ 843 + let response = Requests.One.get ~sw 844 + ~clock:env#clock ~net:env#net 845 + "https://example.com" in 846 + Printf.printf "Status: %d\n" (Requests.Response.status_code response) 847 + ]} *) 814 848 module One = One 815 849 816 850 (** Low-level HTTP client over pooled connections *) ··· 878 912 Example: [Logs.Src.set_level Requests.src (Some Logs.Debug)] *) 879 913 val src : Logs.Src.t 880 914 881 - (** {1 Module-Level Convenience Functions} 882 - 883 - These functions perform one-off requests without creating a session. 884 - They are thin wrappers around {!One} module functions with a simplified 885 - environment-based interface. 886 - 887 - For multiple requests to the same hosts, prefer creating a session with 888 - {!create} to benefit from connection pooling and cookie persistence. 889 - 890 - {b Example:} 891 - {[ 892 - Eio_main.run @@ fun env -> 893 - Eio.Switch.run @@ fun sw -> 894 - let response = Requests.simple_get ~sw env "https://example.com" in 895 - Printf.printf "Status: %d\n" (Response.status_code response) 896 - ]} 897 - *) 898 - 899 - val simple_get : 900 - sw:Eio.Switch.t -> 901 - < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > -> 902 - ?headers:Headers.t -> 903 - ?auth:Auth.t -> 904 - ?timeout:Timeout.t -> 905 - string -> 906 - Response.t 907 - (** [simple_get ~sw env url] performs a one-off GET request. *) 908 - 909 - val simple_post : 910 - sw:Eio.Switch.t -> 911 - < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > -> 912 - ?headers:Headers.t -> 913 - ?body:Body.t -> 914 - ?auth:Auth.t -> 915 - ?timeout:Timeout.t -> 916 - string -> 917 - Response.t 918 - (** [simple_post ~sw env url] performs a one-off POST request. *) 919 - 920 - val simple_put : 921 - sw:Eio.Switch.t -> 922 - < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > -> 923 - ?headers:Headers.t -> 924 - ?body:Body.t -> 925 - ?auth:Auth.t -> 926 - ?timeout:Timeout.t -> 927 - string -> 928 - Response.t 929 - (** [simple_put ~sw env url] performs a one-off PUT request. *) 930 - 931 - val simple_patch : 932 - sw:Eio.Switch.t -> 933 - < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > -> 934 - ?headers:Headers.t -> 935 - ?body:Body.t -> 936 - ?auth:Auth.t -> 937 - ?timeout:Timeout.t -> 938 - string -> 939 - Response.t 940 - (** [simple_patch ~sw env url] performs a one-off PATCH request. *) 941 - 942 - val simple_delete : 943 - sw:Eio.Switch.t -> 944 - < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > -> 945 - ?headers:Headers.t -> 946 - ?auth:Auth.t -> 947 - ?timeout:Timeout.t -> 948 - string -> 949 - Response.t 950 - (** [simple_delete ~sw env url] performs a one-off DELETE request. *) 951 - 952 - val simple_head : 953 - sw:Eio.Switch.t -> 954 - < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > -> 955 - ?headers:Headers.t -> 956 - ?auth:Auth.t -> 957 - ?timeout:Timeout.t -> 958 - string -> 959 - Response.t 960 - (** [simple_head ~sw env url] performs a one-off HEAD request. *)
+3 -1
lib/response.mli
··· 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 - (** HTTP response handling 6 + (** HTTP response handling per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-15}RFC 9110} 7 7 8 8 This module represents HTTP responses and provides functions to access 9 9 status codes, headers, and response bodies. Responses support streaming 10 10 to efficiently handle large payloads. 11 + 12 + Caching semantics follow {{:https://datatracker.ietf.org/doc/html/rfc9111}RFC 9111}} (HTTP Caching). 11 13 12 14 {2 Examples} 13 15
+7 -2
lib/retry.mli
··· 7 7 8 8 This module provides configurable retry logic for HTTP requests, 9 9 including exponential backoff, custom retry predicates, and 10 - Retry-After header support. 10 + Retry-After header support per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-10.2.3}RFC 9110 Section 10.2.3}}. 11 11 12 12 {2 Custom Retry Predicates} 13 13 ··· 105 105 val calculate_backoff : config:config -> attempt:int -> float 106 106 107 107 (** Parse Retry-After header value (seconds or HTTP date). 108 - Per RFC 9110 Section 10.2.3 and Recommendation #5: 108 + 109 + Per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-10.2.3}RFC 9110 Section 10.2.3}}, 110 + Retry-After can be either: 111 + - A non-negative integer (delay in seconds) 112 + - An HTTP-date (absolute time to retry after) 113 + 109 114 Values are capped to [backoff_max] (default 120s) to prevent DoS 110 115 from malicious servers specifying extremely long delays. *) 111 116 val parse_retry_after : ?backoff_max:float -> string -> float option
+13 -1
lib/status.mli
··· 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 - (** HTTP status codes following RFC 7231 and extensions *) 6 + (** HTTP status codes per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-15}RFC 9110 Section 15} 7 + 8 + This module provides types and functions for working with HTTP response 9 + status codes. Status codes are three-digit integers that indicate the 10 + result of an HTTP request. 11 + 12 + {2 Status Code Classes} 13 + 14 + - {b 1xx Informational}: Request received, continuing process 15 + - {b 2xx Success}: Request successfully received, understood, and accepted 16 + - {b 3xx Redirection}: Further action needed to complete the request 17 + - {b 4xx Client Error}: Request contains bad syntax or cannot be fulfilled 18 + - {b 5xx Server Error}: Server failed to fulfill a valid request *) 7 19 8 20 (** Log source for status code operations *) 9 21 val src : Logs.Src.t
+11 -3
test/dune
··· 1 1 (executable 2 2 (name test_localhost) 3 - (libraries conpool eio_main logs logs.fmt)) 3 + (libraries requests conpool eio eio_main fmt fmt.tty logs logs.fmt mirage-crypto-rng.unix)) 4 4 5 5 (executable 6 6 (name test_simple) 7 - (libraries conpool eio_main logs logs.fmt)) 7 + (libraries requests conpool eio eio_main fmt fmt.tty logs logs.fmt mirage-crypto-rng.unix)) 8 8 9 9 (executable 10 10 (name test_one) 11 - (libraries requests eio_main logs logs.fmt mirage-crypto-rng.unix)) 11 + (libraries requests eio eio_main fmt fmt.tty logs logs.fmt mirage-crypto-rng.unix)) 12 + 13 + (executable 14 + (name test_simple_head) 15 + (libraries requests eio eio_main fmt fmt.tty logs logs.fmt mirage-crypto-rng.unix)) 16 + 17 + (executable 18 + (name test_http_date) 19 + (libraries requests eio eio_main alcotest fmt fmt.tty logs logs.fmt ptime)) 12 20 13 21 (cram 14 22 (deps %{bin:ocurl}))
+156 -9
test/httpbin.t
··· 143 143 144 144 Basic authentication (success): 145 145 146 - $ ocurl --verbosity=error -u "user:passwd" "$HTTPBIN_URL/basic-auth/user/passwd" | \ 146 + $ ocurl --verbosity=error --allow-insecure-auth -u "user:passwd" "$HTTPBIN_URL/basic-auth/user/passwd" | \ 147 147 > grep -o '"authenticated": true' 148 148 "authenticated": true 149 149 150 150 Verify username is passed: 151 151 152 - $ ocurl --verbosity=error -u "user:passwd" "$HTTPBIN_URL/basic-auth/user/passwd" | \ 152 + $ ocurl --verbosity=error --allow-insecure-auth -u "user:passwd" "$HTTPBIN_URL/basic-auth/user/passwd" | \ 153 153 > grep -o '"user": "user"' 154 154 "user": "user" 155 155 ··· 274 274 $ ocurl --verbosity=error "$HTTPBIN_URL/headers" | grep -o '"Host": "localhost:8088"' 275 275 "Host": "localhost:8088" 276 276 277 - Verify User-Agent behavior (ocurl does not set User-Agent by default): 277 + Verify User-Agent is set with library default (ocaml-requests): 278 278 279 - $ ocurl --verbosity=error "$HTTPBIN_URL/user-agent" | grep '"user-agent": null' 280 - "user-agent": null 279 + $ ocurl --verbosity=error "$HTTPBIN_URL/user-agent" | grep -o '"user-agent": "ocaml-requests/[^"]*"' 280 + "user-agent": "ocaml-requests/0.1.0 (OCaml 5.4.0)" 281 281 282 282 Check connection header: 283 283 ··· 354 354 355 355 $ SIZE=$(ocurl --verbosity=error -H "Accept-Encoding: gzip" \ 356 356 > "$HTTPBIN_URL/gzip" | wc -c | tr -d ' ') 357 - $ test "$SIZE" -gt 50 && echo "Received compressed gzip data (size: $SIZE bytes)" 358 - Received compressed gzip data (size: 158 bytes) 357 + $ test "$SIZE" -gt 50 && echo "Received compressed gzip data" 358 + Received compressed gzip data 359 359 360 360 Request deflate compressed response (returns binary data): 361 361 362 362 $ SIZE=$(ocurl --verbosity=error -H "Accept-Encoding: deflate" \ 363 363 > "$HTTPBIN_URL/deflate" | wc -c | tr -d ' ') 364 - $ test "$SIZE" -gt 50 && echo "Received compressed deflate data (size: $SIZE bytes)" 365 - Received compressed deflate data (size: 146 bytes) 364 + $ test "$SIZE" -gt 50 && echo "Received compressed deflate data" 365 + Received compressed deflate data 366 366 367 367 Verbose Output Testing 368 368 ---------------------- ··· 498 498 $ ocurl --verbosity=error --timeout 3.0 "$HTTPBIN_URL/delay/1" | \ 499 499 > grep '"url"' > /dev/null && echo "Delay completed successfully" 500 500 Delay completed successfully 501 + 502 + Error Status Code Handling 503 + --------------------------- 504 + 505 + Test 4xx client errors are handled: 506 + 507 + $ ocurl --verbosity=error "$HTTPBIN_URL/status/400" 2>&1 | wc -c | tr -d ' ' 508 + 0 509 + 510 + $ ocurl --verbosity=error "$HTTPBIN_URL/status/404" 2>&1 | wc -c | tr -d ' ' 511 + 0 512 + 513 + $ ocurl --verbosity=error "$HTTPBIN_URL/status/403" 2>&1 | wc -c | tr -d ' ' 514 + 0 515 + 516 + Test 5xx server errors are handled: 517 + 518 + $ ocurl --verbosity=error "$HTTPBIN_URL/status/500" 2>&1 | wc -c | tr -d ' ' 519 + 0 520 + 521 + $ ocurl --verbosity=error "$HTTPBIN_URL/status/502" 2>&1 | wc -c | tr -d ' ' 522 + 0 523 + 524 + $ ocurl --verbosity=error "$HTTPBIN_URL/status/503" 2>&1 | wc -c | tr -d ' ' 525 + 0 526 + 527 + UUID Generation 528 + --------------- 529 + 530 + Test UUID endpoint returns JSON with a UUID field: 531 + 532 + $ ocurl --verbosity=error "$HTTPBIN_URL/uuid" | grep -q '"uuid":' && echo "UUID field present" 533 + UUID field present 534 + 535 + IP Address Endpoint 536 + ------------------- 537 + 538 + Test origin IP endpoint: 539 + 540 + $ ocurl --verbosity=error "$HTTPBIN_URL/ip" | grep -o '"origin":' 541 + "origin": 542 + 543 + Form Data Submission 544 + -------------------- 545 + 546 + POST with data (body is sent as text/plain by default with -d): 547 + 548 + $ ocurl --verbosity=error -X POST -d "username=testuser&password=secret123" \ 549 + > "$HTTPBIN_URL/post" | grep -o '"data": "username=testuser&password=secret123"' 550 + "data": "username=testuser&password=secret123" 551 + 552 + Robots.txt and Deny Endpoints 553 + ----------------------------- 554 + 555 + Test robots.txt endpoint: 556 + 557 + $ ocurl --verbosity=error "$HTTPBIN_URL/robots.txt" | head -2 558 + User-agent: * 559 + Disallow: /deny 560 + 561 + Test deny endpoint returns expected content: 562 + 563 + $ ocurl --verbosity=error "$HTTPBIN_URL/deny" | grep -q "YOU SHOULDN'T BE HERE" && echo "Deny message found" 564 + Deny message found 565 + 566 + Anything Echo Endpoint 567 + ----------------------- 568 + 569 + Test anything endpoint echoes method and data: 570 + 571 + $ ocurl --verbosity=error -X POST -d "echo test" "$HTTPBIN_URL/anything" | \ 572 + > grep -o '"method": "POST"' 573 + "method": "POST" 574 + 575 + $ ocurl --verbosity=error "$HTTPBIN_URL/anything/test/path" | \ 576 + > grep -o '"url": "http://localhost:8088/anything/test/path"' 577 + "url": "http://localhost:8088/anything/test/path" 578 + 579 + Range Requests 580 + -------------- 581 + 582 + Test range endpoint returns specified bytes: 583 + 584 + $ SIZE=$(ocurl --verbosity=error "$HTTPBIN_URL/range/100" | wc -c | tr -d ' ') 585 + $ test "$SIZE" -eq 100 && echo "Range returned exactly 100 bytes" 586 + Range returned exactly 100 bytes 587 + 588 + Links/Pages Endpoint 589 + -------------------- 590 + 591 + Test links endpoint: 592 + 593 + $ ocurl --verbosity=error "$HTTPBIN_URL/links/5/0" | grep -o 'href' | wc -l | tr -d ' ' 594 + 4 595 + 596 + Drip Endpoint (Streaming Data) 597 + ------------------------------- 598 + 599 + Test drip endpoint for streaming data: 600 + 601 + $ SIZE=$(ocurl --verbosity=error --timeout 5.0 "$HTTPBIN_URL/drip?duration=1&numbytes=50&code=200" | wc -c | tr -d ' ') 602 + $ test "$SIZE" -ge 50 && echo "Drip returned expected bytes" 603 + Drip returned expected bytes 604 + 605 + Concurrent URL Fetching 606 + ------------------------ 607 + 608 + Test fetching multiple URLs concurrently (ip and uuid don't have "url" field): 609 + 610 + $ ocurl --verbosity=error "$HTTPBIN_URL/get" "$HTTPBIN_URL/headers" | \ 611 + > grep -c '"Host"' | tr -d ' ' 612 + 2 613 + 614 + HEAD with Response Headers 615 + -------------------------- 616 + 617 + Test HEAD request shows headers with -I flag at info level: 618 + 619 + $ ocurl --verbosity=info -I -X HEAD "$HTTPBIN_URL/get" 2>&1 | grep -o "200 OK" | head -1 620 + 200 OK 621 + 622 + Custom Accept Header 623 + -------------------- 624 + 625 + Test Accept header is passed through: 626 + 627 + $ ocurl --verbosity=error -H "Accept: application/xml" "$HTTPBIN_URL/headers" | \ 628 + > grep -o '"Accept": "application/xml"' 629 + "Accept": "application/xml" 630 + 631 + Large Response Body 632 + ------------------- 633 + 634 + Test handling of larger responses: 635 + 636 + $ SIZE=$(ocurl --verbosity=error "$HTTPBIN_URL/bytes/10000" | wc -c | tr -d ' ') 637 + $ test "$SIZE" -ge 10000 && echo "Large response handled correctly" 638 + Large response handled correctly 639 + 640 + Environment Variables 641 + --------------------- 642 + 643 + Test that environment variables work for configuration: 644 + 645 + $ OCURL_TIMEOUT=2.0 ocurl --verbosity=error "$HTTPBIN_URL/delay/1" | \ 646 + > grep '"url"' > /dev/null && echo "Env var timeout works" 647 + Env var timeout works 501 648 502 649 Cleanup 503 650 -------
+15 -15
test/test_http_date.ml
··· 5 5 6 6 (** Comprehensive tests for HTTP-date parsing per RFC 9110 Section 5.6.7 *) 7 7 8 - open Requests 8 + let parse_http_date = Requests.Response.parse_http_date 9 9 10 10 (** Alcotest testable for Ptime.t *) 11 11 module Alcotest_ptime = struct ··· 25 25 26 26 let test_rfc1123_basic () = 27 27 (* RFC 9110 Section 5.6.7: preferred format "Sun, 06 Nov 1994 08:49:37 GMT" *) 28 - let result = Http_date.parse "Sun, 06 Nov 1994 08:49:37 GMT" in 28 + let result = parse_http_date "Sun, 06 Nov 1994 08:49:37 GMT" in 29 29 let expected = Some (make_time 1994 11 6 8 49 37) in 30 30 Alcotest.(check (option Alcotest_ptime.testable)) 31 31 "RFC 1123 basic parsing" expected result ··· 39 39 ] in 40 40 List.iter (fun (month_str, month_num) -> 41 41 let date_str = Printf.sprintf "Mon, 01 %s 2020 00:00:00 GMT" month_str in 42 - let result = Http_date.parse date_str in 42 + let result = parse_http_date date_str in 43 43 let expected = Some (make_time 2020 month_num 1 0 0 0) in 44 44 Alcotest.(check (option Alcotest_ptime.testable)) 45 45 (Printf.sprintf "RFC 1123 month %s" month_str) expected result ··· 50 50 let weekdays = ["Sun"; "Mon"; "Tue"; "Wed"; "Thu"; "Fri"; "Sat"] in 51 51 List.iter (fun wday -> 52 52 let date_str = Printf.sprintf "%s, 06 Nov 1994 08:49:37 GMT" wday in 53 - let result = Http_date.parse date_str in 53 + let result = parse_http_date date_str in 54 54 let expected = Some (make_time 1994 11 6 8 49 37) in 55 55 Alcotest.(check (option Alcotest_ptime.testable)) 56 56 (Printf.sprintf "RFC 1123 weekday %s" wday) expected result ··· 66 66 ("Fri, 13 Dec 2024 23:59:59 GMT", 2024, 12, 13, 23, 59, 59, "Near current"); 67 67 ] in 68 68 List.iter (fun (date_str, y, m, d, h, min, s, desc) -> 69 - let result = Http_date.parse date_str in 69 + let result = parse_http_date date_str in 70 70 let expected = Some (make_time y m d h min s) in 71 71 Alcotest.(check (option Alcotest_ptime.testable)) 72 72 (Printf.sprintf "RFC 1123 edge: %s" desc) expected result ··· 76 76 77 77 let test_rfc850_basic () = 78 78 (* RFC 850 format: "Sunday, 06-Nov-94 08:49:37 GMT" *) 79 - let result = Http_date.parse "Sunday, 06-Nov-94 08:49:37 GMT" in 79 + let result = parse_http_date "Sunday, 06-Nov-94 08:49:37 GMT" in 80 80 let expected = Some (make_time 1994 11 6 8 49 37) in 81 81 Alcotest.(check (option Alcotest_ptime.testable)) 82 82 "RFC 850 basic parsing (2-digit year)" expected result ··· 91 91 ("Thursday, 01-Jan-69 00:00:00 GMT", 2069, "Year 69 -> 2069"); 92 92 ] in 93 93 List.iter (fun (date_str, expected_year, desc) -> 94 - let result = Http_date.parse date_str in 94 + let result = parse_http_date date_str in 95 95 let expected = Some (make_time expected_year 1 1 0 0 0) in 96 96 Alcotest.(check (option Alcotest_ptime.testable)) 97 97 (Printf.sprintf "RFC 850 %s" desc) expected result ··· 101 101 102 102 let test_asctime_basic () = 103 103 (* asctime() format: "Sun Nov 6 08:49:37 1994" *) 104 - let result = Http_date.parse "Sun Nov 6 08:49:37 1994" in 104 + let result = parse_http_date "Sun Nov 6 08:49:37 1994" in 105 105 let expected = Some (make_time 1994 11 6 8 49 37) in 106 106 Alcotest.(check (option Alcotest_ptime.testable)) 107 107 "asctime basic parsing" expected result ··· 113 113 ("Sun Nov 9 08:49:37 1994", 9, "Day 9"); 114 114 ] in 115 115 List.iter (fun (date_str, day, desc) -> 116 - let result = Http_date.parse date_str in 116 + let result = parse_http_date date_str in 117 117 let expected = Some (make_time 1994 11 day 8 49 37) in 118 118 Alcotest.(check (option Alcotest_ptime.testable)) 119 119 (Printf.sprintf "asctime %s" desc) expected result ··· 131 131 "13-Dec-2024"; (* No day name *) 132 132 ] in 133 133 List.iter (fun input -> 134 - let result = Http_date.parse input in 134 + let result = parse_http_date input in 135 135 Alcotest.(check (option Alcotest_ptime.testable)) 136 136 (Printf.sprintf "Invalid input: %S" input) None result 137 137 ) invalid_inputs ··· 144 144 "Sun, 06 November 1994 08:49:37 GMT"; (* Full month name *) 145 145 ] in 146 146 List.iter (fun input -> 147 - let result = Http_date.parse input in 147 + let result = parse_http_date input in 148 148 Alcotest.(check (option Alcotest_ptime.testable)) 149 149 (Printf.sprintf "Invalid month: %S" input) None result 150 150 ) invalid_months ··· 158 158 "Sun, 31 Apr 2020 00:00:00 GMT"; (* April has 30 days *) 159 159 ] in 160 160 List.iter (fun input -> 161 - let result = Http_date.parse input in 161 + let result = parse_http_date input in 162 162 Alcotest.(check (option Alcotest_ptime.testable)) 163 163 (Printf.sprintf "Invalid date: %S" input) None result 164 164 ) invalid_dates ··· 171 171 "Sun, 06 Nov 1994 00:00:60 GMT"; (* Second 60 (no leap second support) *) 172 172 ] in 173 173 List.iter (fun input -> 174 - let result = Http_date.parse input in 174 + let result = parse_http_date input in 175 175 Alcotest.(check (option Alcotest_ptime.testable)) 176 176 (Printf.sprintf "Invalid time: %S" input) None result 177 177 ) invalid_times ··· 187 187 ] in 188 188 let expected = Some (make_time 1994 11 6 8 49 37) in 189 189 List.iter (fun input -> 190 - let result = Http_date.parse input in 190 + let result = parse_http_date input in 191 191 Alcotest.(check (option Alcotest_ptime.testable)) 192 192 "Whitespace trimming" expected result 193 193 ) test_cases ··· 201 201 ] in 202 202 let expected = Some (make_time 1994 11 6 8 49 37) in 203 203 List.iter (fun (input, desc) -> 204 - let result = Http_date.parse input in 204 + let result = parse_http_date input in 205 205 Alcotest.(check (option Alcotest_ptime.testable)) 206 206 (Printf.sprintf "Case insensitive: %s" desc) expected result 207 207 ) test_cases
+3 -2
test/test_one.ml
··· 1 1 (* Test using One module directly without connection pooling *) 2 2 let () = 3 - Eio_main.run @@ fun env -> 4 3 Mirage_crypto_rng_unix.use_default (); 4 + Eio_main.run @@ fun env -> 5 5 Eio.Switch.run @@ fun sw -> 6 6 try 7 - let response = Requests.simple_get ~sw env "https://opam.ocaml.org" in 7 + let response = Requests.One.get ~sw ~clock:env#clock ~net:env#net 8 + "https://opam.ocaml.org" in 8 9 Printf.printf "Status: %d\n%!" (Requests.Response.status_code response) 9 10 with e -> 10 11 Printf.printf "Exception: %s\n%!" (Printexc.to_string e);
+1
test/test_one.mli
··· 1 + (** Tests for one-shot HTTP requests *)
+4 -3
test/test_simple_head.ml
··· 2 2 let () = 3 3 Logs.set_level (Some Logs.Debug); 4 4 Logs.set_reporter (Logs_fmt.reporter ()); 5 - Eio_main.run @@ fun env -> 6 5 Mirage_crypto_rng_unix.use_default (); 6 + Eio_main.run @@ fun env -> 7 7 Eio.Switch.run @@ fun sw -> 8 - Printf.printf "Making simple_head request...\n%!"; 8 + Printf.printf "Making One.head request...\n%!"; 9 9 try 10 - let response = Requests.simple_head ~sw env "https://opam.ocaml.org" in 10 + let response = Requests.One.head ~sw ~clock:env#clock ~net:env#net 11 + "https://opam.ocaml.org" in 11 12 Printf.printf "Status: %d\n%!" (Requests.Response.status_code response) 12 13 with e -> 13 14 Printf.printf "Exception: %s\n%!" (Printexc.to_string e);
+1
test/test_simple_head.mli
··· 1 + (** Tests for HTTP HEAD requests *)