···11open Eio
22-open Requests
3243let () =
54 Eio_main.run @@ fun env ->
66- Mirage_crypto_rng_unix.use_default ();
55+ Mirage_crypto_rng_eio.run (module Mirage_crypto_rng.Fortuna) env @@ fun () ->
76 Switch.run @@ fun sw ->
8799- (* Example 1: Basic session usage with cookies *)
1010- Printf.printf "\n=== Example 1: Basic Session with Cookies ===\n";
1111- let session = Session.create ~sw env in
88+ (* Example 1: Basic GET request *)
99+ Printf.printf "\n=== Example 1: Basic GET Request ===\n%!";
1010+ let req = Requests.create ~sw env in
1111+ let resp1 = Requests.get req "https://httpbin.org/get" in
1212+ Printf.printf "Status: %d\n%!" (Requests.Response.status_code resp1);
1313+ let body1 = Requests.Response.body resp1 |> Buf_read.of_flow ~max_size:max_int |> Buf_read.take_all in
1414+ Printf.printf "Body length: %d bytes\n%!" (String.length body1);
12151313- (* First request sets a cookie *)
1414- let resp1 = Session.get session "https://httpbin.org/cookies/set?session_id=abc123" in
1515- Printf.printf "Set cookie response: %d\n" (Response.status resp1);
1616+ (* Example 2: POST with JSON body *)
1717+ Printf.printf "\n=== Example 2: POST with JSON ===\n%!";
1818+ let json_data = Jsont.Object ([
1919+ ("name", Jsont.String "Alice");
2020+ ("email", Jsont.String "alice@example.com");
2121+ ("age", Jsont.Number 30.0)
2222+ ], Jsont.Meta.none) in
2323+ let resp2 = Requests.post req
2424+ ~body:(Requests.Body.json json_data)
2525+ "https://httpbin.org/post" in
2626+ Printf.printf "POST status: %d\n%!" (Requests.Response.status_code resp2);
16271717- (* Second request automatically includes the cookie *)
1818- let resp2 = Session.get session "https://httpbin.org/cookies" in
1919- let body2 = Response.body resp2 |> Buf_read.take_all in
2020- Printf.printf "Cookies seen by server: %s\n" body2;
2828+ (* Example 3: Custom headers and authentication *)
2929+ Printf.printf "\n=== Example 3: Custom Headers and Auth ===\n%!";
3030+ let headers = Requests.Headers.empty
3131+ |> Requests.Headers.set "X-Custom-Header" "MyValue"
3232+ |> Requests.Headers.user_agent "OCaml-Requests-Example/1.0" in
3333+ let resp3 = Requests.get req
3434+ ~headers
3535+ ~auth:(Requests.Auth.bearer ~token:"demo-token-123")
3636+ "https://httpbin.org/bearer" in
3737+ Printf.printf "Auth status: %d\n%!" (Requests.Response.status_code resp3);
21382222- (* Example 2: Session with default headers and auth *)
2323- Printf.printf "\n=== Example 2: Session with Default Configuration ===\n";
2424- let github_session = Session.create ~sw env in
3939+ (* Example 4: Session with default headers *)
4040+ Printf.printf "\n=== Example 4: Session with Default Headers ===\n%!";
4141+ let req2 = Requests.create ~sw env in
4242+ let req2 = Requests.set_default_header req2 "User-Agent" "OCaml-Requests/1.0" in
4343+ let req2 = Requests.set_default_header req2 "Accept" "application/json" in
25442626- (* Set default headers that apply to all requests *)
2727- Session.set_default_header github_session "User-Agent" "OCaml-Requests-Example/1.0";
2828- Session.set_default_header github_session "Accept" "application/vnd.github.v3+json";
4545+ (* All requests with req2 will include these headers *)
4646+ let resp4 = Requests.get req2 "https://httpbin.org/headers" in
4747+ Printf.printf "Headers response status: %d\n%!" (Requests.Response.status_code resp4);
29483030- (* Set authentication (if you have a token) *)
3131- (* Session.set_auth github_session (Auth.bearer "your_github_token"); *)
4949+ (* Example 5: Query parameters *)
5050+ Printf.printf "\n=== Example 5: Query Parameters ===\n%!";
5151+ let resp5 = Requests.get req
5252+ ~params:[("key1", "value1"); ("key2", "value2")]
5353+ "https://httpbin.org/get" in
5454+ Printf.printf "Query params status: %d\n%!" (Requests.Response.status_code resp5);
32553333- (* All requests will use these defaults *)
3434- let user = Session.get github_session "https://api.github.com/users/ocaml" in
3535- Printf.printf "GitHub user status: %d\n" (Response.status user);
5656+ (* Example 6: Form data submission *)
5757+ Printf.printf "\n=== Example 6: Form Data ===\n%!";
5858+ let form_body = Requests.Body.form [
5959+ ("username", "demo");
6060+ ("password", "secret123")
6161+ ] in
6262+ let resp6 = Requests.post req
6363+ ~body:form_body
6464+ "https://httpbin.org/post" in
6565+ Printf.printf "Form POST status: %d\n%!" (Requests.Response.status_code resp6);
36663737- (* Example 3: Session with retry logic *)
3838- Printf.printf "\n=== Example 3: Session with Retry Logic ===\n";
3939- let retry_config = Retry.create_config
6767+ (* Example 7: Retry configuration *)
6868+ Printf.printf "\n=== Example 7: Retry Configuration ===\n%!";
6969+ let retry_config = Requests.Retry.create_config
4070 ~max_retries:3
4171 ~backoff_factor:0.5
4242- ~status_forcelist:[429; 500; 502; 503; 504]
4372 () in
7373+ let req_with_retry = Requests.create ~sw ~retry:retry_config env in
7474+ let req_with_retry = Requests.set_timeout req_with_retry
7575+ (Requests.Timeout.create ~total:10.0 ()) in
44764545- let robust_session = Session.create ~sw ~retry:retry_config env in
4646- Session.set_timeout robust_session (Timeout.create ~total:30.0 ());
7777+ (* This will retry on 5xx errors *)
7878+ (try
7979+ let resp7 = Requests.get req_with_retry "https://httpbin.org/status/200" in
8080+ Printf.printf "Retry test status: %d\n%!" (Requests.Response.status_code resp7)
8181+ with _ ->
8282+ Printf.printf "Request failed even after retries\n%!");
47834848- (* This request will automatically retry on failures *)
4949- let result = Session.get robust_session "https://httpbin.org/status/503" in
5050- Printf.printf "Request status (might retry): %d\n" (Response.status result);
8484+ (* Example 8: Concurrent requests using Fiber.both *)
8585+ Printf.printf "\n=== Example 8: Concurrent Requests ===\n%!";
8686+ let start_time = Unix.gettimeofday () in
8787+8888+ let (r1, r2) = Fiber.both
8989+ (fun () -> Requests.get req "https://httpbin.org/delay/1")
9090+ (fun () -> Requests.get req "https://httpbin.org/delay/1") in
9191+9292+ let elapsed = Unix.gettimeofday () -. start_time in
9393+ Printf.printf "Two 1-second delays completed in %.2f seconds (concurrent)\n%!" elapsed;
9494+ Printf.printf "Response 1 status: %d\n%!" (Requests.Response.status_code r1);
9595+ Printf.printf "Response 2 status: %d\n%!" (Requests.Response.status_code r2);
9696+9797+ (* Example 9: One-shot stateless request *)
9898+ Printf.printf "\n=== Example 9: One-Shot Stateless Request ===\n%!";
9999+ let resp9 = Requests.One.get
100100+ ~sw
101101+ ~clock:env#clock
102102+ ~net:env#net
103103+ "https://httpbin.org/get" in
104104+ Printf.printf "One-shot status: %d\n%!" (Requests.Response.status_code resp9);
511055252- (* Example 4: Persistent cookies *)
5353- Printf.printf "\n=== Example 4: Persistent Cookies ===\n";
5454- let persistent_session = Session.create ~sw
5555- ~persist_cookies:true
5656- ~app_name:"ocaml_example"
5757- env in
106106+ (* Example 10: Error handling *)
107107+ Printf.printf "\n=== Example 10: Error Handling ===\n%!";
108108+ (try
109109+ let _resp = Requests.get req "https://httpbin.org/status/404" in
110110+ Printf.printf "Got 404 response (no exception thrown)\n%!"
111111+ with
112112+ | Requests.Error.HTTPError { status; url; _ } ->
113113+ Printf.printf "HTTP Error: %d for %s\n%!" status url
114114+ | Requests.Error.Timeout ->
115115+ Printf.printf "Request timed out\n%!"
116116+ | Requests.Error.ConnectionError msg ->
117117+ Printf.printf "Connection error: %s\n%!" msg
118118+ | exn ->
119119+ Printf.printf "Other error: %s\n%!" (Printexc.to_string exn));
581205959- (* Login and save cookies *)
6060- let _login = Session.post persistent_session
6161- ~form:["username", "demo"; "password", "demo"]
6262- "https://httpbin.org/post" in
121121+ (* Example 11: Timeouts *)
122122+ Printf.printf "\n=== Example 11: Timeouts ===\n%!";
123123+ let req_timeout = Requests.create ~sw env in
124124+ let req_timeout = Requests.set_timeout req_timeout
125125+ (Requests.Timeout.create ~total:5.0 ()) in
631266464- (* Cookies will be saved to ~/.config/ocaml_example/cookies.txt *)
6565- Session.save_cookies persistent_session;
6666- Printf.printf "Cookies saved to disk\n";
127127+ (try
128128+ let resp11 = Requests.get req_timeout "https://httpbin.org/delay/2" in
129129+ Printf.printf "Timeout test completed: %d\n%!" (Requests.Response.status_code resp11)
130130+ with
131131+ | Requests.Error.Timeout ->
132132+ Printf.printf "Request correctly timed out\n%!"
133133+ | exn ->
134134+ Printf.printf "Other timeout error: %s\n%!" (Printexc.to_string exn));
671356868- (* Example 5: Concurrent requests with the same session *)
6969- Printf.printf "\n=== Example 5: Concurrent Requests ===\n";
136136+ (* Example 12: Multiple concurrent requests with Fiber.all *)
137137+ Printf.printf "\n=== Example 12: Multiple Concurrent Requests ===\n%!";
70138 let urls = [
71139 "https://httpbin.org/delay/1";
7272- "https://httpbin.org/delay/1";
7373- "https://httpbin.org/delay/1";
140140+ "https://httpbin.org/get";
141141+ "https://httpbin.org/headers";
74142 ] in
7514376144 let start_time = Unix.gettimeofday () in
7777- let responses = Session.map_concurrent session ~max_concurrent:3
7878- ~f:(fun sess url ->
7979- let resp = Session.get sess url in
8080- Response.status resp
8181- ) urls in
8282-8383- let elapsed = Unix.gettimeofday () -. start_time in
8484- Printf.printf "Concurrent requests completed in %.2fs\n" elapsed;
8585- List.iter (Printf.printf "Status: %d\n") responses;
8686-8787- (* Example 6: Prepared requests *)
8888- Printf.printf "\n=== Example 6: Prepared Requests ===\n";
8989- let prepared = Session.Prepared.create
9090- ~session
9191- ~method_:Method.POST
9292- "https://httpbin.org/post" in
9393-9494- (* Inspect and modify the prepared request *)
9595- let prepared = Session.Prepared.set_header prepared "X-Custom" "Header" in
9696- let prepared = Session.Prepared.set_body prepared (Body.text "Hello, World!") in
9797-9898- Format.printf "Prepared request:@.%a@." Session.Prepared.pp prepared;
9999-100100- (* Send when ready *)
101101- let resp = Session.Prepared.send prepared in
102102- Printf.printf "Prepared request sent, status: %d\n" (Response.status resp);
103103-104104- (* Example 7: Hooks *)
105105- Printf.printf "\n=== Example 7: Request/Response Hooks ===\n";
106106- let hook_session = Session.create ~sw env in
107107-108108- (* Add a request hook to log all requests *)
109109- Session.Hooks.add_request_hook hook_session (fun headers method_ url ->
110110- Printf.printf "-> Request: %s %s\n" (Method.to_string method_) url;
111111- headers
112112- );
113113-114114- (* Add a response hook to log all responses *)
115115- Session.Hooks.add_response_hook hook_session (fun response ->
116116- Printf.printf "<- Response: %d\n" (Response.status response)
117117- );
118118-119119- (* All requests will trigger hooks *)
120120- let _ = Session.get hook_session "https://httpbin.org/get" in
121121- let _ = Session.post hook_session "https://httpbin.org/post" in
122122-123123- (* Example 8: Session statistics *)
124124- Printf.printf "\n=== Example 8: Session Statistics ===\n";
125125- let stats = Session.stats session in
126126- Printf.printf "Total requests: %d\n" stats#requests_made;
127127- Printf.printf "Total time: %.3fs\n" stats#total_time;
128128- Printf.printf "Average time per request: %.3fs\n"
129129- (stats#total_time /. float_of_int stats#requests_made);
145145+ let responses = ref [] in
130146131131- (* Pretty print session info *)
132132- Format.printf "@.Session info:@.%a@." Session.pp session;
147147+ Fiber.all (List.map (fun url ->
148148+ fun () ->
149149+ let resp = Requests.get req url in
150150+ responses := resp :: !responses
151151+ ) urls);
133152134134- (* Example 9: Download file *)
135135- Printf.printf "\n=== Example 9: Download File ===\n";
136136- let download_session = Session.create ~sw env in
137137- let temp_file = Path.(env#fs / "/tmp/example_download.json") in
153153+ let elapsed = Unix.gettimeofday () -. start_time in
154154+ Printf.printf "Three requests completed in %.2f seconds (concurrent)\n%!" elapsed;
155155+ List.iter (fun r ->
156156+ Printf.printf " Status: %d\n%!" (Requests.Response.status_code r)
157157+ ) !responses;
138158139139- Session.download_file download_session
140140- ~on_progress:(fun ~received ~total ->
141141- match total with
142142- | Some t -> Printf.printf "Downloaded %Ld/%Ld bytes\r%!" received t
143143- | None -> Printf.printf "Downloaded %Ld bytes\r%!" received
144144- )
145145- "https://httpbin.org/json"
146146- temp_file;
147147-148148- Printf.printf "\nFile downloaded to /tmp/example_download.json\n";
149149-150150- Printf.printf "\n=== All examples completed successfully! ===\n"159159+ Printf.printf "\n=== All examples completed successfully! ===\n%!"
+6-1
lib/auth.mli
···1616(** Bearer token authentication (e.g., OAuth 2.0) *)
17171818val digest : username:string -> password:string -> t
1919-(** HTTP Digest authentication *)
1919+(** HTTP Digest authentication.
2020+2121+ {b Note:} Digest authentication is currently not fully implemented.
2222+ This function accepts credentials but does not perform the challenge-response
2323+ protocol required for Digest auth. For functional authentication, use
2424+ {!basic} or {!bearer} instead. *)
20252126val custom : (Headers.t -> Headers.t) -> t
2227(** Custom authentication handler *)
+3-7
lib/body.ml
···104104105105let generate_boundary () =
106106 let random_bytes = Mirage_crypto_rng.generate 16 in
107107- let random_part =
108108- Cstruct.to_hex_string (Cstruct.of_string random_bytes)
109109- in
107107+ (* Mirage_crypto_rng.generate returns a string, convert to Cstruct for hex encoding *)
108108+ let random_part = Cstruct.to_hex_string (Cstruct.of_string random_bytes) in
110109 Printf.sprintf "----WebKitFormBoundary%s" random_part
111110112111let multipart parts =
···119118 | Stream { mime; _ } -> Some mime
120119 | File { mime; _ } -> Some mime
121120 | Multipart { boundary; _ } ->
122122- let mime = Mime.make "multipart" "form-data" in
123123- Some (Mime.with_charset boundary mime)
121121+ Some (Mime.multipart_form |> Mime.with_param "boundary" boundary)
124122125123let content_length = function
126124 | Empty -> Some 0L
···271269 | Stream _ -> failwith "Cannot convert streaming body to string for connection pooling (body must be materialized first)"
272270 | File _ -> failwith "Cannot convert file body to string for connection pooling (file must be read first)"
273271 | Multipart _ -> failwith "Cannot convert multipart body to string for connection pooling (must be encoded first)"
274274-275275- let _ = to_string (* Use to avoid warning *)
276272end
+6-3
lib/headers.ml
···1717 | Some (_, values) -> values
1818 | None -> []
1919 in
2020- StringMap.add nkey (key, value :: existing) t
2020+ (* Append to maintain order, avoiding reversal on retrieval *)
2121+ StringMap.add nkey (key, existing @ [value]) t
21222223let set key value t =
2324 let nkey = normalize_key key in
···3233let get_all key t =
3334 let nkey = normalize_key key in
3435 match StringMap.find_opt nkey t with
3535- | Some (_, values) -> List.rev values
3636+ | Some (_, values) -> values
3637 | None -> []
37383839let remove key t =
···48494950let to_list t =
5051 StringMap.fold (fun _ (orig_key, values) acc ->
5151- List.fold_left (fun acc v -> (orig_key, v) :: acc) acc (List.rev values)
5252+ (* Values are already in correct order, build list in reverse then reverse at end *)
5353+ List.fold_left (fun acc v -> (orig_key, v) :: acc) acc values
5254 ) t []
5555+ |> List.rev
53565457let merge t1 t2 =
5558 StringMap.union (fun _ _ v2 -> Some v2) t1 t2
+6-7
lib/http_client.ml
···1818 | None -> failwith "URI must have a host"
1919 in
20202121- let port = match Uri.port uri with
2222- | Some p -> ":" ^ string_of_int p
2323- | None ->
2424- match Uri.scheme uri with
2525- | Some "https" -> ":443"
2626- | Some "http" -> ":80"
2727- | _ -> ""
2121+ (* RFC 7230: default ports should be omitted from Host header *)
2222+ let port = match Uri.port uri, Uri.scheme uri with
2323+ | Some p, Some "https" when p <> 443 -> ":" ^ string_of_int p
2424+ | Some p, Some "http" when p <> 80 -> ":" ^ string_of_int p
2525+ | Some p, _ -> ":" ^ string_of_int p
2626+ | None, _ -> ""
2827 in
29283029 (* Build request line *)
+8
lib/mime.ml
···6767 in
6868 { t with parameters }
69697070+let with_param key value t =
7171+ let key_lower = String.lowercase_ascii key in
7272+ let parameters =
7373+ (key_lower, value) ::
7474+ List.filter (fun (k, _) -> k <> key_lower) t.parameters
7575+ in
7676+ { t with parameters }
7777+7078(* Common MIME types *)
7179let json = make "application" "json"
7280let text = make "text" "plain"
+5
lib/mime.mli
···3030val with_charset : string -> t -> t
3131(** Add or update charset parameter *)
32323333+val with_param : string -> string -> t -> t
3434+(** [with_param key value t] adds or updates a parameter in the MIME type.
3535+ Example: [with_param "boundary" "----WebKit123" multipart_form]
3636+ produces "multipart/form-data; boundary=----WebKit123" *)
3737+3338val charset : t -> string option
3439(** Extract charset parameter if present *)
+51-4
lib/requests.ml
···4242 persist_cookies : bool;
4343 xdg : Xdge.t option;
44444545- (* Statistics - mutable for tracking across all derived sessions *)
4545+ (* Statistics - mutable but NOTE: when sessions are derived via record update
4646+ syntax ({t with field = value}), these are copied not shared. Each derived
4747+ session has independent statistics. Use the same session object to track
4848+ cumulative stats. *)
4649 mutable requests_made : int;
4750 mutable total_time : float;
4851 mutable retries_count : int;
···410413411414 response
412415413413-(* Public request function - executes synchronously *)
416416+(* Public request function - executes synchronously with retry support *)
414417let request t ?headers ?body ?auth ?timeout ?follow_redirects ?max_redirects ~method_ url =
415415- make_request_internal t ?headers ?body ?auth ?timeout
416416- ?follow_redirects ?max_redirects ~method_ url
418418+ match t.retry with
419419+ | None ->
420420+ (* No retry configured, execute directly *)
421421+ make_request_internal t ?headers ?body ?auth ?timeout
422422+ ?follow_redirects ?max_redirects ~method_ url
423423+ | Some retry_config ->
424424+ (* Wrap in retry logic *)
425425+ let should_retry_exn = function
426426+ | Error.Timeout -> true
427427+ | Error.ConnectionError _ -> true
428428+ | Error.SSLError _ -> true
429429+ | _ -> false
430430+ in
431431+432432+ let rec attempt_with_status_retry attempt =
433433+ if attempt > 1 then
434434+ Log.info (fun m -> m "Retry attempt %d/%d for %s %s"
435435+ attempt (retry_config.Retry.max_retries + 1)
436436+ (Method.to_string method_) url);
437437+438438+ try
439439+ let response = make_request_internal t ?headers ?body ?auth ?timeout
440440+ ?follow_redirects ?max_redirects ~method_ url in
441441+ let status = Response.status_code response in
442442+443443+ (* Check if this status code should be retried *)
444444+ if attempt <= retry_config.Retry.max_retries &&
445445+ Retry.should_retry ~config:retry_config ~method_ ~status
446446+ then begin
447447+ let delay = Retry.calculate_backoff ~config:retry_config ~attempt in
448448+ Log.warn (fun m -> m "Request returned status %d (attempt %d/%d). Retrying in %.2f seconds..."
449449+ status attempt (retry_config.Retry.max_retries + 1) delay);
450450+ Eio.Time.sleep t.clock delay;
451451+ t.retries_count <- t.retries_count + 1;
452452+ attempt_with_status_retry (attempt + 1)
453453+ end else
454454+ response
455455+ with exn when attempt <= retry_config.Retry.max_retries && should_retry_exn exn ->
456456+ let delay = Retry.calculate_backoff ~config:retry_config ~attempt in
457457+ Log.warn (fun m -> m "Request failed (attempt %d/%d): %s. Retrying in %.2f seconds..."
458458+ attempt (retry_config.Retry.max_retries + 1) (Printexc.to_string exn) delay);
459459+ Eio.Time.sleep t.clock delay;
460460+ t.retries_count <- t.retries_count + 1;
461461+ attempt_with_status_retry (attempt + 1)
462462+ in
463463+ attempt_with_status_retry 1
417464418465(* Convenience methods *)
419466let get t ?headers ?auth ?timeout ?params url =
+66
lib/requests.mli
···7373 - {b TLS/SSL}: Secure connections with certificate verification
7474 - {b Error Handling}: Comprehensive error types and recovery
75757676+ {2 Error Handling}
7777+7878+ The Requests library uses exceptions as its primary error handling mechanism,
7979+ following Eio's structured concurrency model. This approach ensures that
8080+ errors are propagated cleanly through the switch hierarchy.
8181+8282+ {b Exception-Based Errors:}
8383+8484+ All request functions may raise exceptions from the {!Error} module:
8585+ - {!exception:Error.Timeout}: Request exceeded timeout limit
8686+ - {!exception:Error.ConnectionError}: Network connection failed
8787+ - {!exception:Error.TooManyRedirects}: Exceeded maximum redirect count
8888+ - {!exception:Error.HTTPError}: HTTP error response received (4xx/5xx status)
8989+ - {!exception:Error.SSLError}: TLS/SSL connection error
9090+ - {!exception:Error.AuthenticationError}: Authentication failed
9191+9292+ {b Note on HTTP Status Codes:}
9393+9494+ By default, the library does {b NOT} raise exceptions for HTTP error status
9595+ codes (4xx, 5xx). The response is returned normally and you should check
9696+ the status code explicitly:
9797+9898+ {[
9999+ let resp = Requests.get req "https://api.example.com/data" in
100100+ if Requests.Response.ok resp then
101101+ (* Success: 2xx status *)
102102+ let body = Requests.Response.body resp |> Eio.Flow.read_all in
103103+ process_success body
104104+ else
105105+ (* Error: non-2xx status *)
106106+ let status = Requests.Response.status_code resp in
107107+ handle_error status
108108+ ]}
109109+110110+ To automatically retry on certain HTTP status codes, configure retry behavior:
111111+112112+ {[
113113+ let retry_config = Requests.Retry.create_config
114114+ ~max_retries:3
115115+ ~status_forcelist:[429; 500; 502; 503; 504] (* Retry these codes *)
116116+ () in
117117+ let req = Requests.create ~sw ~retry:retry_config env in
118118+ ]}
119119+120120+ {b Catching Exceptions:}
121121+122122+ {[
123123+ try
124124+ let resp = Requests.get req url in
125125+ handle_success resp
126126+ with
127127+ | Requests.Error.Timeout ->
128128+ (* Handle timeout specifically *)
129129+ retry_with_longer_timeout ()
130130+ | Requests.Error.ConnectionError msg ->
131131+ (* Handle connection errors *)
132132+ log_error "Connection failed: %s" msg
133133+ | exn ->
134134+ (* Handle other errors *)
135135+ log_error "Unexpected error: %s" (Printexc.to_string exn)
136136+ ]}
137137+138138+ The {!Error} module also provides a Result-based API for functional error
139139+ handling, though the primary API uses exceptions for better integration
140140+ with Eio's structured concurrency.
141141+76142 {2 Common Use Cases}
7714378144 {b Working with JSON APIs:}
+4-1
lib/response.ml
···1919 if not response.closed then begin
2020 Log.debug (fun m -> m "Auto-closing response for %s via switch" url);
2121 response.closed <- true;
2222- (* TODO Body cleanup is handled by the underlying HTTP library but test this *)
2222+ (* Body cleanup happens automatically via Eio switch lifecycle.
2323+ The body flow (created via Eio.Flow.string_source) is a memory-backed
2424+ source that doesn't require explicit cleanup. File-based responses
2525+ would have their file handles cleaned up by the switch. *)
2326 end
2427 );
2528