···115115116116 Fmt.pf ppf "@."
117117118118-(* Main function using Session *)
118118+(* Process a single URL and return result *)
119119+let process_url env req method_ headers body include_headers quiet output url_str =
120120+ let uri = Uri.of_string url_str in
121121+122122+ if not quiet then begin
123123+ let method_str = Requests.Method.to_string (method_ :> Requests.Method.t) in
124124+ Fmt.pr "@[<v>%a %a@]@."
125125+ Fmt.(styled `Bold string) method_str
126126+ Fmt.(styled `Underline Uri.pp) uri;
127127+ end;
128128+ try
129129+ (* Make request *)
130130+ let response =
131131+ match method_ with
132132+ | `GET -> Requests.get req ~headers url_str
133133+ | `POST -> Requests.post req ~headers ?body url_str
134134+ | `PUT -> Requests.put req ~headers ?body url_str
135135+ | `DELETE -> Requests.delete req ~headers url_str
136136+ | `HEAD -> Requests.head req ~headers url_str
137137+ | `OPTIONS -> Requests.options req ~headers url_str
138138+ | `PATCH -> Requests.patch req ~headers ?body url_str
139139+ in
140140+141141+ (* Print response headers if requested *)
142142+ if include_headers && not quiet then
143143+ pp_response Fmt.stdout response;
144144+145145+ (* Handle output *)
146146+ let body_flow = Requests.Response.body response in
147147+148148+ begin match output with
149149+ | Some file -> begin
150150+ let filename =
151151+ if List.length [url_str] > 1 then begin
152152+ let base = Filename.remove_extension file in
153153+ let ext = Filename.extension file in
154154+ let url_hash =
155155+ let full_hash = Digest.string url_str |> Digest.to_hex in
156156+ String.sub full_hash (String.length full_hash - 8) 8 in
157157+ Printf.sprintf "%s-%s%s" base url_hash ext
158158+ end else file
159159+ in
160160+ let () =
161161+ Eio.Path.with_open_out ~create:(`Or_truncate 0o644)
162162+ Eio.Path.(env#fs / filename) @@ fun sink ->
163163+ Eio.Flow.copy body_flow sink in
164164+ let () = if not quiet then
165165+ Fmt.pr "[%s] Saved to %s@." url_str filename else () in
166166+ Ok (url_str, response)
167167+ end
168168+ | None ->
169169+ (* Write to stdout *)
170170+ let buf = Buffer.create 1024 in
171171+ Eio.Flow.copy body_flow (Eio.Flow.buffer_sink buf);
172172+ let body_str = Buffer.contents buf in
173173+174174+ (* Pretty-print JSON if applicable *)
175175+ if String.length body_str > 0 &&
176176+ (body_str.[0] = '{' || body_str.[0] = '[') then
177177+ try
178178+ let json = Yojson.Safe.from_string body_str in
179179+ if not quiet then Fmt.pr "[%s]:@." url_str;
180180+ Fmt.pr "%a@." (Yojson.Safe.pretty_print ~std:true) json
181181+ with _ ->
182182+ if not quiet then Fmt.pr "[%s]:@." url_str;
183183+ print_string body_str
184184+ else begin
185185+ if not quiet then Fmt.pr "[%s]:@." url_str;
186186+ print_string body_str
187187+ end;
188188+189189+ if not quiet && Requests.Response.ok response then
190190+ Logs.app (fun m -> m "✓ Success for %s" url_str);
191191+192192+ Ok (url_str, response)
193193+ end
194194+ with
195195+ | exn ->
196196+ if not quiet then
197197+ Logs.err (fun m -> m "Request failed for %s: %s" url_str (Printexc.to_string exn));
198198+ Error (url_str, exn)
199199+200200+(* Main function using Requests with concurrent fetching *)
119201let run_request env sw persist_cookies verify_tls timeout follow_redirects max_redirects
120202 method_ urls headers data json_data output include_headers
121203 auth verbose quiet _show_progress () =
···131213 (* Create XDG paths *)
132214 let xdg = Xdge.create env#fs "ocurl" in
133215134134- (* Create session with configuration *)
216216+ (* Create requests instance with configuration *)
135217 let timeout_obj = Option.map (fun t -> Requests.Timeout.create ~total:t ()) timeout in
136136- let session = Requests.Session.create ~sw ~xdg ~persist_cookies ~verify_tls
218218+ let req = Requests.create ~sw ~xdg ~persist_cookies ~verify_tls
137219 ~follow_redirects ~max_redirects ?timeout:timeout_obj env in
138220139221 (* Set authentication if provided *)
···141223 | Some auth_str ->
142224 (match parse_auth auth_str with
143225 | Some (user, pass) ->
144144- Requests.Session.set_auth session
226226+ Requests.set_auth req
145227 (Requests.Auth.basic ~username:user ~password:pass)
146228 | None ->
147229 Logs.warn (fun m -> m "Invalid auth format, ignoring"))
148230 | None -> ());
149231150150- (* Process each URL *)
151151- List.iter (fun url_str ->
152152- let uri = Uri.of_string url_str in
232232+ (* Build headers from command line *)
233233+ let cmd_headers = List.fold_left (fun hdrs header_str ->
234234+ match parse_header header_str with
235235+ | Some (k, v) -> Requests.Headers.add k v hdrs
236236+ | None -> hdrs
237237+ ) Requests.Headers.empty headers in
153238154154- if not quiet then
155155- let method_str = Requests.Method.to_string (method_ :> Requests.Method.t) in
156156- Fmt.pr "@[<v>%a %a@]@."
157157- Fmt.(styled `Bold string) method_str
158158- Fmt.(styled `Underline Uri.pp) uri;
239239+ (* Prepare body based on data/json options *)
240240+ let body = match json_data, data with
241241+ | Some json, _ -> Some (Requests.Body.json json)
242242+ | None, Some d -> Some (Requests.Body.text d)
243243+ | None, None -> None
244244+ in
159245160160- (* Build headers from command line *)
161161- let cmd_headers = List.fold_left (fun hdrs header_str ->
162162- match parse_header header_str with
163163- | Some (k, v) -> Requests.Headers.add k v hdrs
164164- | None -> hdrs
165165- ) Requests.Headers.empty headers in
246246+ (* Process URLs concurrently or sequentially based on count *)
247247+ match urls with
248248+ | [] -> ()
249249+ | [single_url] ->
250250+ (* Single URL - process directly *)
251251+ let _ = process_url env req method_ cmd_headers body include_headers quiet output single_url in
252252+ ()
253253+ | multiple_urls ->
254254+ (* Multiple URLs - process concurrently *)
255255+ if not quiet then
256256+ Fmt.pr "@[<v>Processing %d URLs concurrently...@]@." (List.length multiple_urls);
166257167167- (* Prepare body based on data/json options *)
168168- let body = match json_data, data with
169169- | Some json, _ -> Some (Requests.Body.json json)
170170- | None, Some d -> Some (Requests.Body.text d)
171171- | None, None -> None
172172- in
173173-174174- try
175175- (* Make request using session *)
176176- let response =
177177- match method_ with
178178- | `GET -> Requests.Session.get session ~headers:cmd_headers url_str
179179- | `POST -> Requests.Session.post session ~headers:cmd_headers ?body url_str
180180- | `PUT -> Requests.Session.put session ~headers:cmd_headers ?body url_str
181181- | `DELETE -> Requests.Session.delete session ~headers:cmd_headers url_str
182182- | `HEAD -> Requests.Session.head session ~headers:cmd_headers url_str
183183- | `OPTIONS -> Requests.Session.options session ~headers:cmd_headers url_str
184184- | `PATCH -> Requests.Session.patch session ~headers:cmd_headers ?body url_str
258258+ (* Create promises for each URL *)
259259+ let results =
260260+ List.map (fun url_str ->
261261+ let promise, resolver = Eio.Promise.create () in
262262+ (* Fork a fiber for each URL *)
263263+ Fiber.fork ~sw (fun () ->
264264+ let result = process_url env req method_ cmd_headers body include_headers quiet output url_str in
265265+ Eio.Promise.resolve resolver result
266266+ );
267267+ promise
268268+ ) multiple_urls
185269 in
186270187187- (* Print response headers if requested *)
188188- if include_headers && not quiet then
189189- pp_response Fmt.stdout response;
271271+ (* Wait for all promises to complete *)
272272+ let completed_results = List.map Eio.Promise.await results in
190273191191- (* Handle output *)
192192- let body_flow = Requests.Response.body response in
274274+ (* Report summary *)
275275+ if not quiet then begin
276276+ let successes = List.filter Result.is_ok completed_results |> List.length in
277277+ let failures = List.filter Result.is_error completed_results |> List.length in
278278+ Fmt.pr "@[<v>@.Summary: %d successful, %d failed out of %d total@]@."
279279+ successes failures (List.length completed_results);
193280194194- match output with
195195- | Some file ->
196196- (* Write to file *)
197197- Eio.Path.with_open_out ~create:(`Or_truncate 0o644)
198198- Eio.Path.(env#fs / file) @@ fun sink ->
199199- Eio.Flow.copy body_flow sink;
200200- if not quiet then
201201- Fmt.pr "Saved to %s@." file
202202- | None ->
203203- (* Write to stdout *)
204204- let buf = Buffer.create 1024 in
205205- Eio.Flow.copy body_flow (Eio.Flow.buffer_sink buf);
206206- let body_str = Buffer.contents buf in
207207-208208- (* Try to pretty-print JSON if it looks like JSON *)
209209- if String.length body_str > 0 &&
210210- (body_str.[0] = '{' || body_str.[0] = '[') then
211211- try
212212- let json = Yojson.Safe.from_string body_str in
213213- Fmt.pr "%a@." (Yojson.Safe.pretty_print ~std:true) json
214214- with _ ->
215215- print_string body_str
216216- else
217217- print_string body_str;
218218-219219- (* Response auto-closes with switch *)
220220-221221- if not quiet && Requests.Response.ok response then
222222- Logs.app (fun m -> m "✓ Success")
223223-224224- with
225225- | exn ->
226226- if not quiet then
227227- Logs.err (fun m -> m "Request failed: %s" (Printexc.to_string exn));
228228- exit 1
229229- ) urls
281281+ (* Print failed URLs *)
282282+ if failures > 0 then begin
283283+ Fmt.pr "@[<v>Failed URLs:@]@.";
284284+ List.iter (function
285285+ | Error (url, _) -> Fmt.pr " - %s@." url
286286+ | Ok _ -> ()
287287+ ) completed_results
288288+ end
289289+ end
230290231291(* Main entry point *)
232292let main method_ urls headers data json_data output include_headers
···243303244304(* Command-line interface *)
245305let cmd =
246246- let doc = "OCaml HTTP client using the Requests library" in
306306+ let doc = "OCaml HTTP client with concurrent fetching using the Requests library" in
247307 let man = [
248308 `S Manpage.s_description;
249309 `P "$(tname) is a command-line HTTP client written in OCaml that uses the \
250250- Requests library with session management. It supports various HTTP methods, \
251251- custom headers, authentication, cookies, and JSON data.";
310310+ Requests library with stateful request management. It supports various HTTP methods, \
311311+ custom headers, authentication, cookies, and JSON data. When multiple URLs are provided, \
312312+ they are fetched concurrently using Eio fibers for maximum performance.";
252313 `S Manpage.s_examples;
253314 `P "Fetch a URL:";
254315 `Pre " $(tname) https://api.github.com";
316316+ `P "Fetch multiple URLs concurrently:";
317317+ `Pre " $(tname) https://api.github.com https://httpbin.org/get https://example.com";
255318 `P "POST JSON data:";
256319 `Pre " $(tname) -X POST --json '{\"key\":\"value\"}' https://httpbin.org/post";
257320 `P "Download file:";
258321 `Pre " $(tname) -o file.zip https://example.com/file.zip";
322322+ `P "Download multiple files concurrently:";
323323+ `Pre " $(tname) -o output.json https://api1.example.com https://api2.example.com https://api3.example.com";
259324 `P "Basic authentication:";
260325 `Pre " $(tname) -u user:pass https://httpbin.org/basic-auth/user/pass";
261326 `P "Custom headers:";
···266331 `Pre " $(tname) --no-verify-tls https://self-signed.example.com";
267332 ] in
268333269269- (* Build the term with Session configuration options *)
334334+ (* Build the term with Requests configuration options *)
270335 let app_name = "ocurl" in
271336 let combined_term =
272337 Term.(const main $ http_method $ urls $ headers $ data $ json_data $
273338 output_file $ include_headers $ auth $ verbose $ quiet $
274339 show_progress $
275275- Requests.Session.Cmd.persist_cookies_term app_name $
276276- Requests.Session.Cmd.verify_tls_term app_name $
277277- Requests.Session.Cmd.timeout_term app_name $
278278- Requests.Session.Cmd.follow_redirects_term app_name $
279279- Requests.Session.Cmd.max_redirects_term app_name $
340340+ Requests.Cmd.persist_cookies_term app_name $
341341+ Requests.Cmd.verify_tls_term app_name $
342342+ Requests.Cmd.timeout_term app_name $
343343+ Requests.Cmd.follow_redirects_term app_name $
344344+ Requests.Cmd.max_redirects_term app_name $
280345 setup_log)
281346 in
282347283348 let info = Cmd.info "ocurl" ~version:"2.0.0" ~doc ~man in
284349 Cmd.v info combined_term
285350286286-let () = exit (Cmd.eval cmd)351351+let () = exit (Cmd.eval cmd)
+8-1
stack/requests/lib/auth.ml
···11+let src = Logs.Src.create "requests.auth" ~doc:"HTTP Authentication"
22+module Log = (val Logs.src_log src : Logs.LOG)
33+14type t =
25 | None
36 | Basic of { username : string; password : string }
···1922 match auth with
2023 | None -> headers
2124 | Basic { username; password } ->
2525+ Log.debug (fun m -> m "Applying basic authentication for user: %s" username);
2226 Headers.basic ~username ~password headers
2327 | Bearer { token } ->
2828+ Log.debug (fun m -> m "Applying bearer token authentication");
2429 Headers.bearer token headers
2525- | Digest { username = _; password = _ } ->
3030+ | Digest { username; password = _ } ->
3131+ Log.debug (fun m -> m "Digest auth configured for user: %s (requires server challenge)" username);
2632 (* Digest auth requires server challenge first, handled elsewhere *)
2733 headers
2834 | Custom f ->
3535+ Log.debug (fun m -> m "Applying custom authentication handler");
2936 f headers
+3
stack/requests/lib/auth.mli
···11(** Authentication mechanisms *)
2233+(** Log source for authentication operations *)
44+val src : Logs.Src.t
55+36type t
47(** Abstract authentication type *)
58
+14-5
stack/requests/lib/body.ml
···11+let src = Logs.Src.create "requests.body" ~doc:"HTTP Request/Response Body"
22+module Log = (val Logs.src_log src : Logs.LOG)
33+14type 'a part = {
25 name : string;
36 filename : string option;
···2629 | None ->
2730 (* Guess MIME type from filename if available *)
2831 let path = Eio.Path.native_exn file in
2929- if String.ends_with ~suffix:".json" path then Mime.json
3030- else if String.ends_with ~suffix:".html" path then Mime.html
3131- else if String.ends_with ~suffix:".xml" path then Mime.xml
3232- else if String.ends_with ~suffix:".txt" path then Mime.text
3333- else Mime.octet_stream
3232+ let guessed =
3333+ if String.ends_with ~suffix:".json" path then Mime.json
3434+ else if String.ends_with ~suffix:".html" path then Mime.html
3535+ else if String.ends_with ~suffix:".xml" path then Mime.xml
3636+ else if String.ends_with ~suffix:".txt" path then Mime.text
3737+ else Mime.octet_stream
3838+ in
3939+ Log.debug (fun m -> m "Guessed MIME type %s for file %s" (Mime.to_string guessed) path);
4040+ guessed
3441 in
4242+ Log.debug (fun m -> m "Creating file body from %s with MIME type %s"
4343+ (Eio.Path.native_exn file) (Mime.to_string mime));
3544 File { file; mime }
36453746let json json_string =
+3
stack/requests/lib/body.mli
···3333 ]}
3434*)
35353636+(** Log source for body operations *)
3737+val src : Logs.Src.t
3838+3639type t
3740(** Abstract body type representing HTTP request body content. *)
3841
-302
stack/requests/lib/client.ml
···11-type ('a,'b) t = {
22- clock : 'a;
33- net : 'b;
44- default_headers : Headers.t;
55- timeout : Timeout.t;
66- max_retries : int;
77- retry_backoff : float;
88- verify_tls : bool;
99- tls_config : Tls.Config.client option;
1010-}
1111-1212-let create
1313- ?(default_headers = Headers.empty)
1414- ?(timeout = Timeout.default)
1515- ?(max_retries = 3)
1616- ?(retry_backoff = 2.0)
1717- ?(verify_tls = true)
1818- ?tls_config
1919- ~clock
2020- ~net
2121- () =
2222- (* Create default TLS config if verify_tls is true and no custom config provided *)
2323- let tls_config =
2424- match tls_config, verify_tls with
2525- | Some config, _ -> Some config
2626- | None, true ->
2727- (* Use CA certificates for verification *)
2828- (match Ca_certs.authenticator () with
2929- | Ok authenticator ->
3030- (match Tls.Config.client ~authenticator () with
3131- | Ok cfg -> Some cfg
3232- | Error (`Msg msg) ->
3333- Logs.warn (fun m -> m "Failed to create TLS config: %s" msg);
3434- None)
3535- | Error (`Msg msg) ->
3636- Logs.warn (fun m -> m "Failed to load CA certificates: %s" msg);
3737- None)
3838- | None, false -> None
3939- in
4040- {
4141- clock;
4242- net;
4343- default_headers;
4444- timeout;
4545- max_retries;
4646- retry_backoff;
4747- verify_tls;
4848- tls_config;
4949- }
5050-5151-let default ~clock ~net =
5252- create ~clock ~net ()
5353-5454-(* Accessors *)
5555-let clock t = t.clock
5656-let net t = t.net
5757-let default_headers t = t.default_headers
5858-let timeout t = t.timeout
5959-let max_retries t = t.max_retries
6060-let retry_backoff t = t.retry_backoff
6161-let verify_tls t = t.verify_tls
6262-let tls_config t = t.tls_config
6363-6464-(* HTTP Request Methods *)
6565-6666-let src = Logs.Src.create "requests.client" ~doc:"HTTP Request Client"
6767-module Log = (val Logs.src_log src : Logs.LOG)
6868-6969-(* Helper to get client or use default *)
7070-let get_client client =
7171- match client with
7272- | Some c -> c
7373- | None -> failwith "No client provided"
7474-7575-(* Convert our Headers.t to Cohttp.Header.t *)
7676-let headers_to_cohttp headers =
7777- Headers.to_list headers
7878- |> Cohttp.Header.of_list
7979-8080-(* Convert Cohttp.Header.t to our Headers.t *)
8181-let headers_from_cohttp cohttp_headers =
8282- Cohttp.Header.to_list cohttp_headers
8383- |> Headers.of_list
8484-8585-(* Main request implementation *)
8686-let request ~sw ?client ?headers ?body ?auth ?timeout ?follow_redirects
8787- ?max_redirects ~method_ url =
8888- let client = get_client client in
8989- let start_time = Unix.gettimeofday () in
9090-9191- Log.info (fun m -> m "Making %s request to %s" (Method.to_string method_) url);
9292-9393- (* Prepare headers *)
9494- let headers = match headers with
9595- | Some h -> h
9696- | None -> default_headers client
9797- in
9898-9999- (* Apply auth *)
100100- let headers = match auth with
101101- | Some a ->
102102- Log.debug (fun m -> m "Applying authentication");
103103- Auth.apply a headers
104104- | None -> headers
105105- in
106106-107107- (* Add content type from body *)
108108- let headers = match body with
109109- | Some b -> (match Body.content_type b with
110110- | Some mime -> Headers.content_type mime headers
111111- | None -> headers)
112112- | None -> headers
113113- in
114114-115115- (* Convert to Cohttp types *)
116116- let cohttp_method =
117117- match Method.to_string method_ with
118118- | "GET" -> `GET
119119- | "POST" -> `POST
120120- | "PUT" -> `PUT
121121- | "DELETE" -> `DELETE
122122- | "HEAD" -> `HEAD
123123- | "OPTIONS" -> `OPTIONS
124124- | "PATCH" -> `PATCH
125125- | "CONNECT" -> `CONNECT
126126- | "TRACE" -> `TRACE
127127- | _ -> `GET
128128- in
129129-130130- let cohttp_headers = headers_to_cohttp headers in
131131- let cohttp_body = match body with
132132- | Some b -> Body.Private.to_cohttp_body ~sw b
133133- | None -> None
134134- in
135135-136136- (* Make request using cohttp-eio *)
137137- let uri = Uri.of_string url in
138138-139139- (* Create HTTPS handler if TLS is configured *)
140140- let https = match tls_config client with
141141- | None ->
142142- Log.debug (fun m -> m "No TLS configuration");
143143- None
144144- | Some tls_config ->
145145- Log.debug (fun m -> m "Using TLS configuration");
146146- let https_fn uri socket =
147147- let host =
148148- Uri.host uri
149149- |> Option.map (fun x -> Domain_name.(host_exn (of_string_exn x)))
150150- in
151151- Tls_eio.client_of_flow ?host tls_config socket
152152- in
153153- Some https_fn
154154- in
155155-156156- (* Create the client *)
157157- let eio_client = Cohttp_eio.Client.make ~https (net client) in
158158-159159- (* Apply timeout if specified *)
160160- let make_request () =
161161- Cohttp_eio.Client.call ~sw eio_client cohttp_method uri ~headers:cohttp_headers ?body:cohttp_body
162162- in
163163-164164- (* Make the actual request with optional timeout *)
165165- let resp, resp_body =
166166- match timeout with
167167- | Some t ->
168168- let timeout_seconds = Timeout.total t in
169169- (match timeout_seconds with
170170- | Some seconds ->
171171- Log.debug (fun m -> m "Setting timeout: %.2f seconds" seconds);
172172- Eio.Time.with_timeout_exn (clock client) seconds make_request
173173- | None -> make_request ())
174174- | None -> make_request ()
175175- in
176176-177177- let status = Cohttp.Response.status resp |> Cohttp.Code.code_of_status in
178178- let cohttp_resp_headers = Cohttp.Response.headers resp in
179179- let resp_headers = headers_from_cohttp cohttp_resp_headers in
180180-181181- Log.info (fun m -> m "Received response: status=%d" status);
182182-183183- (* Handle redirects if enabled *)
184184- let follow_redirects = Option.value follow_redirects ~default:true in
185185- let max_redirects = Option.value max_redirects ~default:10 in
186186-187187- let final_resp, final_body, final_url =
188188- if follow_redirects && (status >= 300 && status < 400) then
189189- let rec follow_redirect url redirects_left =
190190- if redirects_left <= 0 then begin
191191- Log.err (fun m -> m "Too many redirects (%d) for %s" max_redirects url);
192192- raise (Error.TooManyRedirects { url; count = max_redirects; max = max_redirects })
193193- end else
194194- (* Get location header from Cohttp headers *)
195195- match Cohttp.Header.get cohttp_resp_headers "location" with
196196- | None ->
197197- Log.debug (fun m -> m "Redirect response missing Location header");
198198- (resp, resp_body, url)
199199- | Some location ->
200200- Log.info (fun m -> m "Following redirect to %s (%d remaining)"
201201- location redirects_left);
202202- (* Make new request to redirect location *)
203203- let new_uri = Uri.of_string location in
204204- let new_resp, new_body =
205205- Cohttp_eio.Client.call ~sw eio_client cohttp_method new_uri ~headers:cohttp_headers
206206- in
207207- let new_status = Cohttp.Response.status new_resp |> Cohttp.Code.code_of_status in
208208- if new_status >= 300 && new_status < 400 then
209209- follow_redirect location (redirects_left - 1)
210210- else
211211- (new_resp, new_body, location)
212212- in
213213- follow_redirect url max_redirects
214214- else
215215- (resp, resp_body, url)
216216- in
217217-218218- (* Get final headers *)
219219- let final_headers =
220220- if final_resp == resp then
221221- resp_headers
222222- else
223223- Cohttp.Response.headers final_resp |> headers_from_cohttp
224224- in
225225-226226- let elapsed = Unix.gettimeofday () -. start_time in
227227- Log.info (fun m -> m "Request completed in %.3f seconds" elapsed);
228228-229229- Response.Private.make
230230- ~sw
231231- ~status
232232- ~headers:final_headers
233233- ~body:final_body
234234- ~url:final_url
235235- ~elapsed
236236-237237-(* Convenience methods *)
238238-let get ~sw ?client ?headers ?auth ?timeout ?follow_redirects ?max_redirects url =
239239- request ~sw ?client ?headers ?auth ?timeout ?follow_redirects ?max_redirects
240240- ~method_:`GET url
241241-242242-let post ~sw ?client ?headers ?body ?auth ?timeout url =
243243- request ~sw ?client ?headers ?body ?auth ?timeout ~method_:`POST url
244244-245245-let put ~sw ?client ?headers ?body ?auth ?timeout url =
246246- request ~sw ?client ?headers ?body ?auth ?timeout ~method_:`PUT url
247247-248248-let delete ~sw ?client ?headers ?auth ?timeout url =
249249- request ~sw ?client ?headers ?auth ?timeout ~method_:`DELETE url
250250-251251-let head ~sw ?client ?headers ?auth ?timeout url =
252252- request ~sw ?client ?headers ?auth ?timeout ~method_:`HEAD url
253253-254254-let patch ~sw ?client ?headers ?body ?auth ?timeout url =
255255- request ~sw ?client ?headers ?body ?auth ?timeout ~method_:`PATCH url
256256-257257-let upload ~sw ?client ?headers ?auth ?timeout ?method_ ?mime ?length
258258- ?on_progress ~source url =
259259- let method_ = Option.value method_ ~default:`POST in
260260- let mime = Option.value mime ~default:Mime.octet_stream in
261261-262262- (* Wrap source with progress tracking if callback provided *)
263263- let tracked_source = match on_progress with
264264- | None -> source
265265- | Some callback ->
266266- (* For now, progress tracking is not implemented for uploads
267267- due to complexity of wrapping Eio.Flow.source.
268268- This would require creating a custom flow wrapper. *)
269269- let _ = callback in
270270- source
271271- in
272272-273273- let body = Body.of_stream ?length mime tracked_source in
274274- request ~sw ?client ?headers ~body ?auth ?timeout ~method_ url
275275-276276-let download ~sw ?client ?headers ?auth ?timeout ?on_progress url ~sink =
277277- let response = get ~sw ?client ?headers ?auth ?timeout url in
278278-279279- try
280280- (* Get content length for progress tracking *)
281281- let total = Response.content_length response in
282282-283283- let body = Response.body response in
284284-285285- (* Stream data to sink with optional progress *)
286286- match on_progress with
287287- | None ->
288288- (* No progress tracking, just copy directly *)
289289- Eio.Flow.copy body sink
290290- | Some progress_fn ->
291291- (* Copy with progress tracking *)
292292- (* We need to intercept the flow to track bytes *)
293293- (* For now, just do a simple copy - proper progress tracking needs flow wrapper *)
294294- progress_fn ~received:0L ~total;
295295- Eio.Flow.copy body sink;
296296- progress_fn ~received:(Option.value total ~default:0L) ~total;
297297-298298- (* Response auto-closes with switch *)
299299- ()
300300- with e ->
301301- (* Response auto-closes with switch *)
302302- raise e
-202
stack/requests/lib/client.mli
···11-(** Low-level HTTP client with streaming support
22-33- The Client module provides a stateless HTTP client with connection pooling,
44- TLS support, and streaming capabilities. For stateful requests with automatic
55- cookie handling and persistent configuration, use the {!Session} module instead.
66-77- {2 Examples}
88-99- {[
1010- open Eio_main
1111-1212- let () = run @@ fun env ->
1313- Switch.run @@ fun sw ->
1414-1515- (* Create a client *)
1616- let client = Client.create ~clock:env#clock ~net:env#net () in
1717-1818- (* Simple GET request *)
1919- let response = Client.get ~sw ~client "https://example.com" in
2020- Printf.printf "Status: %d\n" (Response.status_code response);
2121- Response.close response;
2222-2323- (* POST with JSON body *)
2424- let response = Client.post ~sw ~client
2525- ~body:(Body.json {|{"key": "value"}|})
2626- ~headers:(Headers.empty |> Headers.content_type Mime.json)
2727- "https://api.example.com/data" in
2828- Response.close response;
2929-3030- (* Download file with streaming *)
3131- Client.download ~sw ~client
3232- "https://example.com/large-file.zip"
3333- ~sink:(Eio.Path.(fs / "download.zip" |> sink))
3434- ]}
3535-*)
3636-3737-type ('a,'b) t
3838-(** Client configuration with clock and network types.
3939- The type parameters track the Eio environment capabilities. *)
4040-4141-(** {1 Client Creation} *)
4242-4343-val create :
4444- ?default_headers:Headers.t ->
4545- ?timeout:Timeout.t ->
4646- ?max_retries:int ->
4747- ?retry_backoff:float ->
4848- ?verify_tls:bool ->
4949- ?tls_config:Tls.Config.client ->
5050- clock:'a Eio.Time.clock ->
5151- net:'b Eio.Net.t ->
5252- unit -> ('a Eio.Time.clock, 'b Eio.Net.t) t
5353-(** [create ?default_headers ?timeout ?max_retries ?retry_backoff ?verify_tls ?tls_config ~clock ~net ()]
5454- creates a new HTTP client with the specified configuration.
5555-5656- @param default_headers Headers to include in every request (default: empty)
5757- @param timeout Default timeout configuration (default: 30s connect, 60s read)
5858- @param max_retries Maximum number of retries for failed requests (default: 3)
5959- @param retry_backoff Exponential backoff factor for retries (default: 2.0)
6060- @param verify_tls Whether to verify TLS certificates (default: true)
6161- @param tls_config Custom TLS configuration (default: uses system CA certificates)
6262- @param clock Eio clock for timeouts and scheduling
6363- @param net Eio network capability for making connections
6464-*)
6565-6666-val default : clock:'a Eio.Time.clock -> net:'b Eio.Net.t -> ('a Eio.Time.clock, 'b Eio.Net.t) t
6767-(** [default ~clock ~net] creates a client with default configuration.
6868- Equivalent to [create ~clock ~net ()]. *)
6969-7070-(** {1 Configuration Access} *)
7171-7272-val clock : ('a,'b) t -> 'a
7373-(** [clock client] returns the clock capability. *)
7474-7575-val net : ('a,'b) t -> 'b
7676-(** [net client] returns the network capability. *)
7777-7878-val default_headers : ('a,'b) t -> Headers.t
7979-(** [default_headers client] returns the default headers. *)
8080-8181-val timeout : ('a,'b) t -> Timeout.t
8282-(** [timeout client] returns the timeout configuration. *)
8383-8484-val max_retries : ('a,'b) t -> int
8585-(** [max_retries client] returns the maximum retry count. *)
8686-8787-val retry_backoff : ('a,'b) t -> float
8888-(** [retry_backoff client] returns the retry backoff factor. *)
8989-9090-val verify_tls : ('a,'b) t -> bool
9191-(** [verify_tls client] returns whether TLS verification is enabled. *)
9292-9393-val tls_config : ('a,'b) t -> Tls.Config.client option
9494-(** [tls_config client] returns the TLS configuration if set. *)
9595-9696-(** {1 HTTP Request Methods} *)
9797-9898-val request :
9999- sw:Eio.Switch.t ->
100100- ?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
101101- ?headers:Headers.t ->
102102- ?body:Body.t ->
103103- ?auth:Auth.t ->
104104- ?timeout:Timeout.t ->
105105- ?follow_redirects:bool ->
106106- ?max_redirects:int ->
107107- method_:Method.t ->
108108- string ->
109109- Response.t
110110-(** Make a streaming request *)
111111-112112-val get :
113113- sw:Eio.Switch.t ->
114114- ?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
115115- ?headers:Headers.t ->
116116- ?auth:Auth.t ->
117117- ?timeout:Timeout.t ->
118118- ?follow_redirects:bool ->
119119- ?max_redirects:int ->
120120- string ->
121121- Response.t
122122-(** GET request *)
123123-124124-val post :
125125- sw:Eio.Switch.t ->
126126- ?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
127127- ?headers:Headers.t ->
128128- ?body:Body.t ->
129129- ?auth:Auth.t ->
130130- ?timeout:Timeout.t ->
131131- string ->
132132- Response.t
133133-(** POST request *)
134134-135135-val put :
136136- sw:Eio.Switch.t ->
137137- ?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
138138- ?headers:Headers.t ->
139139- ?body:Body.t ->
140140- ?auth:Auth.t ->
141141- ?timeout:Timeout.t ->
142142- string ->
143143- Response.t
144144-(** PUT request *)
145145-146146-val delete :
147147- sw:Eio.Switch.t ->
148148- ?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
149149- ?headers:Headers.t ->
150150- ?auth:Auth.t ->
151151- ?timeout:Timeout.t ->
152152- string ->
153153- Response.t
154154-(** DELETE request *)
155155-156156-val head :
157157- sw:Eio.Switch.t ->
158158- ?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
159159- ?headers:Headers.t ->
160160- ?auth:Auth.t ->
161161- ?timeout:Timeout.t ->
162162- string ->
163163- Response.t
164164-(** HEAD request *)
165165-166166-val patch :
167167- sw:Eio.Switch.t ->
168168- ?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
169169- ?headers:Headers.t ->
170170- ?body:Body.t ->
171171- ?auth:Auth.t ->
172172- ?timeout:Timeout.t ->
173173- string ->
174174- Response.t
175175-(** PATCH request *)
176176-177177-val upload :
178178- sw:Eio.Switch.t ->
179179- ?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
180180- ?headers:Headers.t ->
181181- ?auth:Auth.t ->
182182- ?timeout:Timeout.t ->
183183- ?method_:Method.t ->
184184- ?mime:Mime.t ->
185185- ?length:int64 ->
186186- ?on_progress:(sent:int64 -> total:int64 option -> unit) ->
187187- source:Eio.Flow.source_ty Eio.Resource.t ->
188188- string ->
189189- Response.t
190190-(** Upload from stream *)
191191-192192-val download :
193193- sw:Eio.Switch.t ->
194194- ?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
195195- ?headers:Headers.t ->
196196- ?auth:Auth.t ->
197197- ?timeout:Timeout.t ->
198198- ?on_progress:(received:int64 -> total:int64 option -> unit) ->
199199- string ->
200200- sink:Eio.Flow.sink_ty Eio.Resource.t ->
201201- unit
202202-(** Download to stream *)
···11(** Centralized error handling for the Requests library *)
2233+(** Log source for error reporting *)
44+val src : Logs.Src.t
55+36(** {1 Exception Types} *)
4758(** Raised when a request times out *)
+3
stack/requests/lib/headers.mli
···1515 ]}
1616*)
17171818+(** Log source for header operations *)
1919+val src : Logs.Src.t
2020+1821type t
1922(** Abstract header collection type. Headers are stored with case-insensitive
2023 keys and maintain insertion order. *)