A batteries included HTTP/1.1 client in OCaml

sync

+284 -143
+129 -120
examples/session_example.ml
··· 1 1 open Eio 2 - open Requests 3 2 4 3 let () = 5 4 Eio_main.run @@ fun env -> 6 - Mirage_crypto_rng_unix.use_default (); 5 + Mirage_crypto_rng_eio.run (module Mirage_crypto_rng.Fortuna) env @@ fun () -> 7 6 Switch.run @@ fun sw -> 8 7 9 - (* Example 1: Basic session usage with cookies *) 10 - Printf.printf "\n=== Example 1: Basic Session with Cookies ===\n"; 11 - let session = Session.create ~sw env in 8 + (* Example 1: Basic GET request *) 9 + Printf.printf "\n=== Example 1: Basic GET Request ===\n%!"; 10 + let req = Requests.create ~sw env in 11 + let resp1 = Requests.get req "https://httpbin.org/get" in 12 + Printf.printf "Status: %d\n%!" (Requests.Response.status_code resp1); 13 + let body1 = Requests.Response.body resp1 |> Buf_read.of_flow ~max_size:max_int |> Buf_read.take_all in 14 + Printf.printf "Body length: %d bytes\n%!" (String.length body1); 12 15 13 - (* First request sets a cookie *) 14 - let resp1 = Session.get session "https://httpbin.org/cookies/set?session_id=abc123" in 15 - Printf.printf "Set cookie response: %d\n" (Response.status resp1); 16 + (* Example 2: POST with JSON body *) 17 + Printf.printf "\n=== Example 2: POST with JSON ===\n%!"; 18 + let json_data = Jsont.Object ([ 19 + ("name", Jsont.String "Alice"); 20 + ("email", Jsont.String "alice@example.com"); 21 + ("age", Jsont.Number 30.0) 22 + ], Jsont.Meta.none) in 23 + let resp2 = Requests.post req 24 + ~body:(Requests.Body.json json_data) 25 + "https://httpbin.org/post" in 26 + Printf.printf "POST status: %d\n%!" (Requests.Response.status_code resp2); 16 27 17 - (* Second request automatically includes the cookie *) 18 - let resp2 = Session.get session "https://httpbin.org/cookies" in 19 - let body2 = Response.body resp2 |> Buf_read.take_all in 20 - Printf.printf "Cookies seen by server: %s\n" body2; 28 + (* Example 3: Custom headers and authentication *) 29 + Printf.printf "\n=== Example 3: Custom Headers and Auth ===\n%!"; 30 + let headers = Requests.Headers.empty 31 + |> Requests.Headers.set "X-Custom-Header" "MyValue" 32 + |> Requests.Headers.user_agent "OCaml-Requests-Example/1.0" in 33 + let resp3 = Requests.get req 34 + ~headers 35 + ~auth:(Requests.Auth.bearer ~token:"demo-token-123") 36 + "https://httpbin.org/bearer" in 37 + Printf.printf "Auth status: %d\n%!" (Requests.Response.status_code resp3); 21 38 22 - (* Example 2: Session with default headers and auth *) 23 - Printf.printf "\n=== Example 2: Session with Default Configuration ===\n"; 24 - let github_session = Session.create ~sw env in 39 + (* Example 4: Session with default headers *) 40 + Printf.printf "\n=== Example 4: Session with Default Headers ===\n%!"; 41 + let req2 = Requests.create ~sw env in 42 + let req2 = Requests.set_default_header req2 "User-Agent" "OCaml-Requests/1.0" in 43 + let req2 = Requests.set_default_header req2 "Accept" "application/json" in 25 44 26 - (* Set default headers that apply to all requests *) 27 - Session.set_default_header github_session "User-Agent" "OCaml-Requests-Example/1.0"; 28 - Session.set_default_header github_session "Accept" "application/vnd.github.v3+json"; 45 + (* All requests with req2 will include these headers *) 46 + let resp4 = Requests.get req2 "https://httpbin.org/headers" in 47 + Printf.printf "Headers response status: %d\n%!" (Requests.Response.status_code resp4); 29 48 30 - (* Set authentication (if you have a token) *) 31 - (* Session.set_auth github_session (Auth.bearer "your_github_token"); *) 49 + (* Example 5: Query parameters *) 50 + Printf.printf "\n=== Example 5: Query Parameters ===\n%!"; 51 + let resp5 = Requests.get req 52 + ~params:[("key1", "value1"); ("key2", "value2")] 53 + "https://httpbin.org/get" in 54 + Printf.printf "Query params status: %d\n%!" (Requests.Response.status_code resp5); 32 55 33 - (* All requests will use these defaults *) 34 - let user = Session.get github_session "https://api.github.com/users/ocaml" in 35 - Printf.printf "GitHub user status: %d\n" (Response.status user); 56 + (* Example 6: Form data submission *) 57 + Printf.printf "\n=== Example 6: Form Data ===\n%!"; 58 + let form_body = Requests.Body.form [ 59 + ("username", "demo"); 60 + ("password", "secret123") 61 + ] in 62 + let resp6 = Requests.post req 63 + ~body:form_body 64 + "https://httpbin.org/post" in 65 + Printf.printf "Form POST status: %d\n%!" (Requests.Response.status_code resp6); 36 66 37 - (* Example 3: Session with retry logic *) 38 - Printf.printf "\n=== Example 3: Session with Retry Logic ===\n"; 39 - let retry_config = Retry.create_config 67 + (* Example 7: Retry configuration *) 68 + Printf.printf "\n=== Example 7: Retry Configuration ===\n%!"; 69 + let retry_config = Requests.Retry.create_config 40 70 ~max_retries:3 41 71 ~backoff_factor:0.5 42 - ~status_forcelist:[429; 500; 502; 503; 504] 43 72 () in 73 + let req_with_retry = Requests.create ~sw ~retry:retry_config env in 74 + let req_with_retry = Requests.set_timeout req_with_retry 75 + (Requests.Timeout.create ~total:10.0 ()) in 44 76 45 - let robust_session = Session.create ~sw ~retry:retry_config env in 46 - Session.set_timeout robust_session (Timeout.create ~total:30.0 ()); 77 + (* This will retry on 5xx errors *) 78 + (try 79 + let resp7 = Requests.get req_with_retry "https://httpbin.org/status/200" in 80 + Printf.printf "Retry test status: %d\n%!" (Requests.Response.status_code resp7) 81 + with _ -> 82 + Printf.printf "Request failed even after retries\n%!"); 47 83 48 - (* This request will automatically retry on failures *) 49 - let result = Session.get robust_session "https://httpbin.org/status/503" in 50 - Printf.printf "Request status (might retry): %d\n" (Response.status result); 84 + (* Example 8: Concurrent requests using Fiber.both *) 85 + Printf.printf "\n=== Example 8: Concurrent Requests ===\n%!"; 86 + let start_time = Unix.gettimeofday () in 87 + 88 + let (r1, r2) = Fiber.both 89 + (fun () -> Requests.get req "https://httpbin.org/delay/1") 90 + (fun () -> Requests.get req "https://httpbin.org/delay/1") in 91 + 92 + let elapsed = Unix.gettimeofday () -. start_time in 93 + Printf.printf "Two 1-second delays completed in %.2f seconds (concurrent)\n%!" elapsed; 94 + Printf.printf "Response 1 status: %d\n%!" (Requests.Response.status_code r1); 95 + Printf.printf "Response 2 status: %d\n%!" (Requests.Response.status_code r2); 96 + 97 + (* Example 9: One-shot stateless request *) 98 + Printf.printf "\n=== Example 9: One-Shot Stateless Request ===\n%!"; 99 + let resp9 = Requests.One.get 100 + ~sw 101 + ~clock:env#clock 102 + ~net:env#net 103 + "https://httpbin.org/get" in 104 + Printf.printf "One-shot status: %d\n%!" (Requests.Response.status_code resp9); 51 105 52 - (* Example 4: Persistent cookies *) 53 - Printf.printf "\n=== Example 4: Persistent Cookies ===\n"; 54 - let persistent_session = Session.create ~sw 55 - ~persist_cookies:true 56 - ~app_name:"ocaml_example" 57 - env in 106 + (* Example 10: Error handling *) 107 + Printf.printf "\n=== Example 10: Error Handling ===\n%!"; 108 + (try 109 + let _resp = Requests.get req "https://httpbin.org/status/404" in 110 + Printf.printf "Got 404 response (no exception thrown)\n%!" 111 + with 112 + | Requests.Error.HTTPError { status; url; _ } -> 113 + Printf.printf "HTTP Error: %d for %s\n%!" status url 114 + | Requests.Error.Timeout -> 115 + Printf.printf "Request timed out\n%!" 116 + | Requests.Error.ConnectionError msg -> 117 + Printf.printf "Connection error: %s\n%!" msg 118 + | exn -> 119 + Printf.printf "Other error: %s\n%!" (Printexc.to_string exn)); 58 120 59 - (* Login and save cookies *) 60 - let _login = Session.post persistent_session 61 - ~form:["username", "demo"; "password", "demo"] 62 - "https://httpbin.org/post" in 121 + (* Example 11: Timeouts *) 122 + Printf.printf "\n=== Example 11: Timeouts ===\n%!"; 123 + let req_timeout = Requests.create ~sw env in 124 + let req_timeout = Requests.set_timeout req_timeout 125 + (Requests.Timeout.create ~total:5.0 ()) in 63 126 64 - (* Cookies will be saved to ~/.config/ocaml_example/cookies.txt *) 65 - Session.save_cookies persistent_session; 66 - Printf.printf "Cookies saved to disk\n"; 127 + (try 128 + let resp11 = Requests.get req_timeout "https://httpbin.org/delay/2" in 129 + Printf.printf "Timeout test completed: %d\n%!" (Requests.Response.status_code resp11) 130 + with 131 + | Requests.Error.Timeout -> 132 + Printf.printf "Request correctly timed out\n%!" 133 + | exn -> 134 + Printf.printf "Other timeout error: %s\n%!" (Printexc.to_string exn)); 67 135 68 - (* Example 5: Concurrent requests with the same session *) 69 - Printf.printf "\n=== Example 5: Concurrent Requests ===\n"; 136 + (* Example 12: Multiple concurrent requests with Fiber.all *) 137 + Printf.printf "\n=== Example 12: Multiple Concurrent Requests ===\n%!"; 70 138 let urls = [ 71 139 "https://httpbin.org/delay/1"; 72 - "https://httpbin.org/delay/1"; 73 - "https://httpbin.org/delay/1"; 140 + "https://httpbin.org/get"; 141 + "https://httpbin.org/headers"; 74 142 ] in 75 143 76 144 let start_time = Unix.gettimeofday () in 77 - let responses = Session.map_concurrent session ~max_concurrent:3 78 - ~f:(fun sess url -> 79 - let resp = Session.get sess url in 80 - Response.status resp 81 - ) urls in 82 - 83 - let elapsed = Unix.gettimeofday () -. start_time in 84 - Printf.printf "Concurrent requests completed in %.2fs\n" elapsed; 85 - List.iter (Printf.printf "Status: %d\n") responses; 86 - 87 - (* Example 6: Prepared requests *) 88 - Printf.printf "\n=== Example 6: Prepared Requests ===\n"; 89 - let prepared = Session.Prepared.create 90 - ~session 91 - ~method_:Method.POST 92 - "https://httpbin.org/post" in 93 - 94 - (* Inspect and modify the prepared request *) 95 - let prepared = Session.Prepared.set_header prepared "X-Custom" "Header" in 96 - let prepared = Session.Prepared.set_body prepared (Body.text "Hello, World!") in 97 - 98 - Format.printf "Prepared request:@.%a@." Session.Prepared.pp prepared; 99 - 100 - (* Send when ready *) 101 - let resp = Session.Prepared.send prepared in 102 - Printf.printf "Prepared request sent, status: %d\n" (Response.status resp); 103 - 104 - (* Example 7: Hooks *) 105 - Printf.printf "\n=== Example 7: Request/Response Hooks ===\n"; 106 - let hook_session = Session.create ~sw env in 107 - 108 - (* Add a request hook to log all requests *) 109 - Session.Hooks.add_request_hook hook_session (fun headers method_ url -> 110 - Printf.printf "-> Request: %s %s\n" (Method.to_string method_) url; 111 - headers 112 - ); 113 - 114 - (* Add a response hook to log all responses *) 115 - Session.Hooks.add_response_hook hook_session (fun response -> 116 - Printf.printf "<- Response: %d\n" (Response.status response) 117 - ); 118 - 119 - (* All requests will trigger hooks *) 120 - let _ = Session.get hook_session "https://httpbin.org/get" in 121 - let _ = Session.post hook_session "https://httpbin.org/post" in 122 - 123 - (* Example 8: Session statistics *) 124 - Printf.printf "\n=== Example 8: Session Statistics ===\n"; 125 - let stats = Session.stats session in 126 - Printf.printf "Total requests: %d\n" stats#requests_made; 127 - Printf.printf "Total time: %.3fs\n" stats#total_time; 128 - Printf.printf "Average time per request: %.3fs\n" 129 - (stats#total_time /. float_of_int stats#requests_made); 145 + let responses = ref [] in 130 146 131 - (* Pretty print session info *) 132 - Format.printf "@.Session info:@.%a@." Session.pp session; 147 + Fiber.all (List.map (fun url -> 148 + fun () -> 149 + let resp = Requests.get req url in 150 + responses := resp :: !responses 151 + ) urls); 133 152 134 - (* Example 9: Download file *) 135 - Printf.printf "\n=== Example 9: Download File ===\n"; 136 - let download_session = Session.create ~sw env in 137 - let temp_file = Path.(env#fs / "/tmp/example_download.json") in 153 + let elapsed = Unix.gettimeofday () -. start_time in 154 + Printf.printf "Three requests completed in %.2f seconds (concurrent)\n%!" elapsed; 155 + List.iter (fun r -> 156 + Printf.printf " Status: %d\n%!" (Requests.Response.status_code r) 157 + ) !responses; 138 158 139 - Session.download_file download_session 140 - ~on_progress:(fun ~received ~total -> 141 - match total with 142 - | Some t -> Printf.printf "Downloaded %Ld/%Ld bytes\r%!" received t 143 - | None -> Printf.printf "Downloaded %Ld bytes\r%!" received 144 - ) 145 - "https://httpbin.org/json" 146 - temp_file; 147 - 148 - Printf.printf "\nFile downloaded to /tmp/example_download.json\n"; 149 - 150 - Printf.printf "\n=== All examples completed successfully! ===\n" 159 + Printf.printf "\n=== All examples completed successfully! ===\n%!"
+6 -1
lib/auth.mli
··· 16 16 (** Bearer token authentication (e.g., OAuth 2.0) *) 17 17 18 18 val digest : username:string -> password:string -> t 19 - (** HTTP Digest authentication *) 19 + (** HTTP Digest authentication. 20 + 21 + {b Note:} Digest authentication is currently not fully implemented. 22 + This function accepts credentials but does not perform the challenge-response 23 + protocol required for Digest auth. For functional authentication, use 24 + {!basic} or {!bearer} instead. *) 20 25 21 26 val custom : (Headers.t -> Headers.t) -> t 22 27 (** Custom authentication handler *)
+3 -7
lib/body.ml
··· 104 104 105 105 let generate_boundary () = 106 106 let random_bytes = Mirage_crypto_rng.generate 16 in 107 - let random_part = 108 - Cstruct.to_hex_string (Cstruct.of_string random_bytes) 109 - in 107 + (* Mirage_crypto_rng.generate returns a string, convert to Cstruct for hex encoding *) 108 + let random_part = Cstruct.to_hex_string (Cstruct.of_string random_bytes) in 110 109 Printf.sprintf "----WebKitFormBoundary%s" random_part 111 110 112 111 let multipart parts = ··· 119 118 | Stream { mime; _ } -> Some mime 120 119 | File { mime; _ } -> Some mime 121 120 | Multipart { boundary; _ } -> 122 - let mime = Mime.make "multipart" "form-data" in 123 - Some (Mime.with_charset boundary mime) 121 + Some (Mime.multipart_form |> Mime.with_param "boundary" boundary) 124 122 125 123 let content_length = function 126 124 | Empty -> Some 0L ··· 271 269 | Stream _ -> failwith "Cannot convert streaming body to string for connection pooling (body must be materialized first)" 272 270 | File _ -> failwith "Cannot convert file body to string for connection pooling (file must be read first)" 273 271 | Multipart _ -> failwith "Cannot convert multipart body to string for connection pooling (must be encoded first)" 274 - 275 - let _ = to_string (* Use to avoid warning *) 276 272 end
+6 -3
lib/headers.ml
··· 17 17 | Some (_, values) -> values 18 18 | None -> [] 19 19 in 20 - StringMap.add nkey (key, value :: existing) t 20 + (* Append to maintain order, avoiding reversal on retrieval *) 21 + StringMap.add nkey (key, existing @ [value]) t 21 22 22 23 let set key value t = 23 24 let nkey = normalize_key key in ··· 32 33 let get_all key t = 33 34 let nkey = normalize_key key in 34 35 match StringMap.find_opt nkey t with 35 - | Some (_, values) -> List.rev values 36 + | Some (_, values) -> values 36 37 | None -> [] 37 38 38 39 let remove key t = ··· 48 49 49 50 let to_list t = 50 51 StringMap.fold (fun _ (orig_key, values) acc -> 51 - List.fold_left (fun acc v -> (orig_key, v) :: acc) acc (List.rev values) 52 + (* Values are already in correct order, build list in reverse then reverse at end *) 53 + List.fold_left (fun acc v -> (orig_key, v) :: acc) acc values 52 54 ) t [] 55 + |> List.rev 53 56 54 57 let merge t1 t2 = 55 58 StringMap.union (fun _ _ v2 -> Some v2) t1 t2
+6 -7
lib/http_client.ml
··· 18 18 | None -> failwith "URI must have a host" 19 19 in 20 20 21 - let port = match Uri.port uri with 22 - | Some p -> ":" ^ string_of_int p 23 - | None -> 24 - match Uri.scheme uri with 25 - | Some "https" -> ":443" 26 - | Some "http" -> ":80" 27 - | _ -> "" 21 + (* RFC 7230: default ports should be omitted from Host header *) 22 + let port = match Uri.port uri, Uri.scheme uri with 23 + | Some p, Some "https" when p <> 443 -> ":" ^ string_of_int p 24 + | Some p, Some "http" when p <> 80 -> ":" ^ string_of_int p 25 + | Some p, _ -> ":" ^ string_of_int p 26 + | None, _ -> "" 28 27 in 29 28 30 29 (* Build request line *)
+8
lib/mime.ml
··· 67 67 in 68 68 { t with parameters } 69 69 70 + let with_param key value t = 71 + let key_lower = String.lowercase_ascii key in 72 + let parameters = 73 + (key_lower, value) :: 74 + List.filter (fun (k, _) -> k <> key_lower) t.parameters 75 + in 76 + { t with parameters } 77 + 70 78 (* Common MIME types *) 71 79 let json = make "application" "json" 72 80 let text = make "text" "plain"
+5
lib/mime.mli
··· 30 30 val with_charset : string -> t -> t 31 31 (** Add or update charset parameter *) 32 32 33 + val with_param : string -> string -> t -> t 34 + (** [with_param key value t] adds or updates a parameter in the MIME type. 35 + Example: [with_param "boundary" "----WebKit123" multipart_form] 36 + produces "multipart/form-data; boundary=----WebKit123" *) 37 + 33 38 val charset : t -> string option 34 39 (** Extract charset parameter if present *)
+51 -4
lib/requests.ml
··· 42 42 persist_cookies : bool; 43 43 xdg : Xdge.t option; 44 44 45 - (* Statistics - mutable for tracking across all derived sessions *) 45 + (* Statistics - mutable but NOTE: when sessions are derived via record update 46 + syntax ({t with field = value}), these are copied not shared. Each derived 47 + session has independent statistics. Use the same session object to track 48 + cumulative stats. *) 46 49 mutable requests_made : int; 47 50 mutable total_time : float; 48 51 mutable retries_count : int; ··· 410 413 411 414 response 412 415 413 - (* Public request function - executes synchronously *) 416 + (* Public request function - executes synchronously with retry support *) 414 417 let request t ?headers ?body ?auth ?timeout ?follow_redirects ?max_redirects ~method_ url = 415 - make_request_internal t ?headers ?body ?auth ?timeout 416 - ?follow_redirects ?max_redirects ~method_ url 418 + match t.retry with 419 + | None -> 420 + (* No retry configured, execute directly *) 421 + make_request_internal t ?headers ?body ?auth ?timeout 422 + ?follow_redirects ?max_redirects ~method_ url 423 + | Some retry_config -> 424 + (* Wrap in retry logic *) 425 + let should_retry_exn = function 426 + | Error.Timeout -> true 427 + | Error.ConnectionError _ -> true 428 + | Error.SSLError _ -> true 429 + | _ -> false 430 + in 431 + 432 + let rec attempt_with_status_retry attempt = 433 + if attempt > 1 then 434 + Log.info (fun m -> m "Retry attempt %d/%d for %s %s" 435 + attempt (retry_config.Retry.max_retries + 1) 436 + (Method.to_string method_) url); 437 + 438 + try 439 + let response = make_request_internal t ?headers ?body ?auth ?timeout 440 + ?follow_redirects ?max_redirects ~method_ url in 441 + let status = Response.status_code response in 442 + 443 + (* Check if this status code should be retried *) 444 + if attempt <= retry_config.Retry.max_retries && 445 + Retry.should_retry ~config:retry_config ~method_ ~status 446 + then begin 447 + let delay = Retry.calculate_backoff ~config:retry_config ~attempt in 448 + Log.warn (fun m -> m "Request returned status %d (attempt %d/%d). Retrying in %.2f seconds..." 449 + status attempt (retry_config.Retry.max_retries + 1) delay); 450 + Eio.Time.sleep t.clock delay; 451 + t.retries_count <- t.retries_count + 1; 452 + attempt_with_status_retry (attempt + 1) 453 + end else 454 + response 455 + with exn when attempt <= retry_config.Retry.max_retries && should_retry_exn exn -> 456 + let delay = Retry.calculate_backoff ~config:retry_config ~attempt in 457 + Log.warn (fun m -> m "Request failed (attempt %d/%d): %s. Retrying in %.2f seconds..." 458 + attempt (retry_config.Retry.max_retries + 1) (Printexc.to_string exn) delay); 459 + Eio.Time.sleep t.clock delay; 460 + t.retries_count <- t.retries_count + 1; 461 + attempt_with_status_retry (attempt + 1) 462 + in 463 + attempt_with_status_retry 1 417 464 418 465 (* Convenience methods *) 419 466 let get t ?headers ?auth ?timeout ?params url =
+66
lib/requests.mli
··· 73 73 - {b TLS/SSL}: Secure connections with certificate verification 74 74 - {b Error Handling}: Comprehensive error types and recovery 75 75 76 + {2 Error Handling} 77 + 78 + The Requests library uses exceptions as its primary error handling mechanism, 79 + following Eio's structured concurrency model. This approach ensures that 80 + errors are propagated cleanly through the switch hierarchy. 81 + 82 + {b Exception-Based Errors:} 83 + 84 + All request functions may raise exceptions from the {!Error} module: 85 + - {!exception:Error.Timeout}: Request exceeded timeout limit 86 + - {!exception:Error.ConnectionError}: Network connection failed 87 + - {!exception:Error.TooManyRedirects}: Exceeded maximum redirect count 88 + - {!exception:Error.HTTPError}: HTTP error response received (4xx/5xx status) 89 + - {!exception:Error.SSLError}: TLS/SSL connection error 90 + - {!exception:Error.AuthenticationError}: Authentication failed 91 + 92 + {b Note on HTTP Status Codes:} 93 + 94 + By default, the library does {b NOT} raise exceptions for HTTP error status 95 + codes (4xx, 5xx). The response is returned normally and you should check 96 + the status code explicitly: 97 + 98 + {[ 99 + let resp = Requests.get req "https://api.example.com/data" in 100 + if Requests.Response.ok resp then 101 + (* Success: 2xx status *) 102 + let body = Requests.Response.body resp |> Eio.Flow.read_all in 103 + process_success body 104 + else 105 + (* Error: non-2xx status *) 106 + let status = Requests.Response.status_code resp in 107 + handle_error status 108 + ]} 109 + 110 + To automatically retry on certain HTTP status codes, configure retry behavior: 111 + 112 + {[ 113 + let retry_config = Requests.Retry.create_config 114 + ~max_retries:3 115 + ~status_forcelist:[429; 500; 502; 503; 504] (* Retry these codes *) 116 + () in 117 + let req = Requests.create ~sw ~retry:retry_config env in 118 + ]} 119 + 120 + {b Catching Exceptions:} 121 + 122 + {[ 123 + try 124 + let resp = Requests.get req url in 125 + handle_success resp 126 + with 127 + | Requests.Error.Timeout -> 128 + (* Handle timeout specifically *) 129 + retry_with_longer_timeout () 130 + | Requests.Error.ConnectionError msg -> 131 + (* Handle connection errors *) 132 + log_error "Connection failed: %s" msg 133 + | exn -> 134 + (* Handle other errors *) 135 + log_error "Unexpected error: %s" (Printexc.to_string exn) 136 + ]} 137 + 138 + The {!Error} module also provides a Result-based API for functional error 139 + handling, though the primary API uses exceptions for better integration 140 + with Eio's structured concurrency. 141 + 76 142 {2 Common Use Cases} 77 143 78 144 {b Working with JSON APIs:}
+4 -1
lib/response.ml
··· 19 19 if not response.closed then begin 20 20 Log.debug (fun m -> m "Auto-closing response for %s via switch" url); 21 21 response.closed <- true; 22 - (* TODO Body cleanup is handled by the underlying HTTP library but test this *) 22 + (* Body cleanup happens automatically via Eio switch lifecycle. 23 + The body flow (created via Eio.Flow.string_source) is a memory-backed 24 + source that doesn't require explicit cleanup. File-based responses 25 + would have their file handles cleaned up by the switch. *) 23 26 end 24 27 ); 25 28