My agentic slop goes here. Not intended for anyone else!

more

+2 -2
stack/requests/bin/dune
···
(executables
-
(public_names ocurl)
-
(names ocurl)
(libraries requests eio_main cmdliner logs logs.fmt fmt.tty yojson))
···
(executables
+
(public_names ocurl test_logging)
+
(names ocurl test_logging)
(libraries requests eio_main cmdliner logs logs.fmt fmt.tty yojson))
+152 -87
stack/requests/bin/ocurl.ml
···
Fmt.pf ppf "@."
-
(* Main function using Session *)
let run_request env sw persist_cookies verify_tls timeout follow_redirects max_redirects
method_ urls headers data json_data output include_headers
auth verbose quiet _show_progress () =
···
(* Create XDG paths *)
let xdg = Xdge.create env#fs "ocurl" in
-
(* Create session with configuration *)
let timeout_obj = Option.map (fun t -> Requests.Timeout.create ~total:t ()) timeout in
-
let session = Requests.Session.create ~sw ~xdg ~persist_cookies ~verify_tls
~follow_redirects ~max_redirects ?timeout:timeout_obj env in
(* Set authentication if provided *)
···
| Some auth_str ->
(match parse_auth auth_str with
| Some (user, pass) ->
-
Requests.Session.set_auth session
(Requests.Auth.basic ~username:user ~password:pass)
| None ->
Logs.warn (fun m -> m "Invalid auth format, ignoring"))
| None -> ());
-
(* Process each URL *)
-
List.iter (fun url_str ->
-
let uri = Uri.of_string url_str in
-
if not quiet then
-
let method_str = Requests.Method.to_string (method_ :> Requests.Method.t) in
-
Fmt.pr "@[<v>%a %a@]@."
-
Fmt.(styled `Bold string) method_str
-
Fmt.(styled `Underline Uri.pp) uri;
-
(* Build headers from command line *)
-
let cmd_headers = List.fold_left (fun hdrs header_str ->
-
match parse_header header_str with
-
| Some (k, v) -> Requests.Headers.add k v hdrs
-
| None -> hdrs
-
) Requests.Headers.empty headers in
-
(* Prepare body based on data/json options *)
-
let body = match json_data, data with
-
| Some json, _ -> Some (Requests.Body.json json)
-
| None, Some d -> Some (Requests.Body.text d)
-
| None, None -> None
-
in
-
-
try
-
(* Make request using session *)
-
let response =
-
match method_ with
-
| `GET -> Requests.Session.get session ~headers:cmd_headers url_str
-
| `POST -> Requests.Session.post session ~headers:cmd_headers ?body url_str
-
| `PUT -> Requests.Session.put session ~headers:cmd_headers ?body url_str
-
| `DELETE -> Requests.Session.delete session ~headers:cmd_headers url_str
-
| `HEAD -> Requests.Session.head session ~headers:cmd_headers url_str
-
| `OPTIONS -> Requests.Session.options session ~headers:cmd_headers url_str
-
| `PATCH -> Requests.Session.patch session ~headers:cmd_headers ?body url_str
in
-
(* Print response headers if requested *)
-
if include_headers && not quiet then
-
pp_response Fmt.stdout response;
-
(* Handle output *)
-
let body_flow = Requests.Response.body response in
-
match output with
-
| Some file ->
-
(* Write to file *)
-
Eio.Path.with_open_out ~create:(`Or_truncate 0o644)
-
Eio.Path.(env#fs / file) @@ fun sink ->
-
Eio.Flow.copy body_flow sink;
-
if not quiet then
-
Fmt.pr "Saved to %s@." file
-
| None ->
-
(* Write to stdout *)
-
let buf = Buffer.create 1024 in
-
Eio.Flow.copy body_flow (Eio.Flow.buffer_sink buf);
-
let body_str = Buffer.contents buf in
-
-
(* Try to pretty-print JSON if it looks like JSON *)
-
if String.length body_str > 0 &&
-
(body_str.[0] = '{' || body_str.[0] = '[') then
-
try
-
let json = Yojson.Safe.from_string body_str in
-
Fmt.pr "%a@." (Yojson.Safe.pretty_print ~std:true) json
-
with _ ->
-
print_string body_str
-
else
-
print_string body_str;
-
-
(* Response auto-closes with switch *)
-
-
if not quiet && Requests.Response.ok response then
-
Logs.app (fun m -> m "✓ Success")
-
-
with
-
| exn ->
-
if not quiet then
-
Logs.err (fun m -> m "Request failed: %s" (Printexc.to_string exn));
-
exit 1
-
) urls
(* Main entry point *)
let main method_ urls headers data json_data output include_headers
···
(* Command-line interface *)
let cmd =
-
let doc = "OCaml HTTP client using the Requests library" in
let man = [
`S Manpage.s_description;
`P "$(tname) is a command-line HTTP client written in OCaml that uses the \
-
Requests library with session management. It supports various HTTP methods, \
-
custom headers, authentication, cookies, and JSON data.";
`S Manpage.s_examples;
`P "Fetch a URL:";
`Pre " $(tname) https://api.github.com";
`P "POST JSON data:";
`Pre " $(tname) -X POST --json '{\"key\":\"value\"}' https://httpbin.org/post";
`P "Download file:";
`Pre " $(tname) -o file.zip https://example.com/file.zip";
`P "Basic authentication:";
`Pre " $(tname) -u user:pass https://httpbin.org/basic-auth/user/pass";
`P "Custom headers:";
···
`Pre " $(tname) --no-verify-tls https://self-signed.example.com";
] in
-
(* Build the term with Session configuration options *)
let app_name = "ocurl" in
let combined_term =
Term.(const main $ http_method $ urls $ headers $ data $ json_data $
output_file $ include_headers $ auth $ verbose $ quiet $
show_progress $
-
Requests.Session.Cmd.persist_cookies_term app_name $
-
Requests.Session.Cmd.verify_tls_term app_name $
-
Requests.Session.Cmd.timeout_term app_name $
-
Requests.Session.Cmd.follow_redirects_term app_name $
-
Requests.Session.Cmd.max_redirects_term app_name $
setup_log)
in
let info = Cmd.info "ocurl" ~version:"2.0.0" ~doc ~man in
Cmd.v info combined_term
-
let () = exit (Cmd.eval cmd)
···
Fmt.pf ppf "@."
+
(* Process a single URL and return result *)
+
let process_url env req method_ headers body include_headers quiet output url_str =
+
let uri = Uri.of_string url_str in
+
+
if not quiet then begin
+
let method_str = Requests.Method.to_string (method_ :> Requests.Method.t) in
+
Fmt.pr "@[<v>%a %a@]@."
+
Fmt.(styled `Bold string) method_str
+
Fmt.(styled `Underline Uri.pp) uri;
+
end;
+
try
+
(* Make request *)
+
let response =
+
match method_ with
+
| `GET -> Requests.get req ~headers url_str
+
| `POST -> Requests.post req ~headers ?body url_str
+
| `PUT -> Requests.put req ~headers ?body url_str
+
| `DELETE -> Requests.delete req ~headers url_str
+
| `HEAD -> Requests.head req ~headers url_str
+
| `OPTIONS -> Requests.options req ~headers url_str
+
| `PATCH -> Requests.patch req ~headers ?body url_str
+
in
+
+
(* Print response headers if requested *)
+
if include_headers && not quiet then
+
pp_response Fmt.stdout response;
+
+
(* Handle output *)
+
let body_flow = Requests.Response.body response in
+
+
begin match output with
+
| Some file -> begin
+
let filename =
+
if List.length [url_str] > 1 then begin
+
let base = Filename.remove_extension file in
+
let ext = Filename.extension file in
+
let url_hash =
+
let full_hash = Digest.string url_str |> Digest.to_hex in
+
String.sub full_hash (String.length full_hash - 8) 8 in
+
Printf.sprintf "%s-%s%s" base url_hash ext
+
end else file
+
in
+
let () =
+
Eio.Path.with_open_out ~create:(`Or_truncate 0o644)
+
Eio.Path.(env#fs / filename) @@ fun sink ->
+
Eio.Flow.copy body_flow sink in
+
let () = if not quiet then
+
Fmt.pr "[%s] Saved to %s@." url_str filename else () in
+
Ok (url_str, response)
+
end
+
| None ->
+
(* Write to stdout *)
+
let buf = Buffer.create 1024 in
+
Eio.Flow.copy body_flow (Eio.Flow.buffer_sink buf);
+
let body_str = Buffer.contents buf in
+
+
(* Pretty-print JSON if applicable *)
+
if String.length body_str > 0 &&
+
(body_str.[0] = '{' || body_str.[0] = '[') then
+
try
+
let json = Yojson.Safe.from_string body_str in
+
if not quiet then Fmt.pr "[%s]:@." url_str;
+
Fmt.pr "%a@." (Yojson.Safe.pretty_print ~std:true) json
+
with _ ->
+
if not quiet then Fmt.pr "[%s]:@." url_str;
+
print_string body_str
+
else begin
+
if not quiet then Fmt.pr "[%s]:@." url_str;
+
print_string body_str
+
end;
+
+
if not quiet && Requests.Response.ok response then
+
Logs.app (fun m -> m "✓ Success for %s" url_str);
+
+
Ok (url_str, response)
+
end
+
with
+
| exn ->
+
if not quiet then
+
Logs.err (fun m -> m "Request failed for %s: %s" url_str (Printexc.to_string exn));
+
Error (url_str, exn)
+
+
(* Main function using Requests with concurrent fetching *)
let run_request env sw persist_cookies verify_tls timeout follow_redirects max_redirects
method_ urls headers data json_data output include_headers
auth verbose quiet _show_progress () =
···
(* Create XDG paths *)
let xdg = Xdge.create env#fs "ocurl" in
+
(* Create requests instance with configuration *)
let timeout_obj = Option.map (fun t -> Requests.Timeout.create ~total:t ()) timeout in
+
let req = Requests.create ~sw ~xdg ~persist_cookies ~verify_tls
~follow_redirects ~max_redirects ?timeout:timeout_obj env in
(* Set authentication if provided *)
···
| Some auth_str ->
(match parse_auth auth_str with
| Some (user, pass) ->
+
Requests.set_auth req
(Requests.Auth.basic ~username:user ~password:pass)
| None ->
Logs.warn (fun m -> m "Invalid auth format, ignoring"))
| None -> ());
+
(* Build headers from command line *)
+
let cmd_headers = List.fold_left (fun hdrs header_str ->
+
match parse_header header_str with
+
| Some (k, v) -> Requests.Headers.add k v hdrs
+
| None -> hdrs
+
) Requests.Headers.empty headers in
+
(* Prepare body based on data/json options *)
+
let body = match json_data, data with
+
| Some json, _ -> Some (Requests.Body.json json)
+
| None, Some d -> Some (Requests.Body.text d)
+
| None, None -> None
+
in
+
(* Process URLs concurrently or sequentially based on count *)
+
match urls with
+
| [] -> ()
+
| [single_url] ->
+
(* Single URL - process directly *)
+
let _ = process_url env req method_ cmd_headers body include_headers quiet output single_url in
+
()
+
| multiple_urls ->
+
(* Multiple URLs - process concurrently *)
+
if not quiet then
+
Fmt.pr "@[<v>Processing %d URLs concurrently...@]@." (List.length multiple_urls);
+
(* Create promises for each URL *)
+
let results =
+
List.map (fun url_str ->
+
let promise, resolver = Eio.Promise.create () in
+
(* Fork a fiber for each URL *)
+
Fiber.fork ~sw (fun () ->
+
let result = process_url env req method_ cmd_headers body include_headers quiet output url_str in
+
Eio.Promise.resolve resolver result
+
);
+
promise
+
) multiple_urls
in
+
(* Wait for all promises to complete *)
+
let completed_results = List.map Eio.Promise.await results in
+
(* Report summary *)
+
if not quiet then begin
+
let successes = List.filter Result.is_ok completed_results |> List.length in
+
let failures = List.filter Result.is_error completed_results |> List.length in
+
Fmt.pr "@[<v>@.Summary: %d successful, %d failed out of %d total@]@."
+
successes failures (List.length completed_results);
+
(* Print failed URLs *)
+
if failures > 0 then begin
+
Fmt.pr "@[<v>Failed URLs:@]@.";
+
List.iter (function
+
| Error (url, _) -> Fmt.pr " - %s@." url
+
| Ok _ -> ()
+
) completed_results
+
end
+
end
(* Main entry point *)
let main method_ urls headers data json_data output include_headers
···
(* Command-line interface *)
let cmd =
+
let doc = "OCaml HTTP client with concurrent fetching using the Requests library" in
let man = [
`S Manpage.s_description;
`P "$(tname) is a command-line HTTP client written in OCaml that uses the \
+
Requests library with stateful request management. It supports various HTTP methods, \
+
custom headers, authentication, cookies, and JSON data. When multiple URLs are provided, \
+
they are fetched concurrently using Eio fibers for maximum performance.";
`S Manpage.s_examples;
`P "Fetch a URL:";
`Pre " $(tname) https://api.github.com";
+
`P "Fetch multiple URLs concurrently:";
+
`Pre " $(tname) https://api.github.com https://httpbin.org/get https://example.com";
`P "POST JSON data:";
`Pre " $(tname) -X POST --json '{\"key\":\"value\"}' https://httpbin.org/post";
`P "Download file:";
`Pre " $(tname) -o file.zip https://example.com/file.zip";
+
`P "Download multiple files concurrently:";
+
`Pre " $(tname) -o output.json https://api1.example.com https://api2.example.com https://api3.example.com";
`P "Basic authentication:";
`Pre " $(tname) -u user:pass https://httpbin.org/basic-auth/user/pass";
`P "Custom headers:";
···
`Pre " $(tname) --no-verify-tls https://self-signed.example.com";
] in
+
(* Build the term with Requests configuration options *)
let app_name = "ocurl" in
let combined_term =
Term.(const main $ http_method $ urls $ headers $ data $ json_data $
output_file $ include_headers $ auth $ verbose $ quiet $
show_progress $
+
Requests.Cmd.persist_cookies_term app_name $
+
Requests.Cmd.verify_tls_term app_name $
+
Requests.Cmd.timeout_term app_name $
+
Requests.Cmd.follow_redirects_term app_name $
+
Requests.Cmd.max_redirects_term app_name $
setup_log)
in
let info = Cmd.info "ocurl" ~version:"2.0.0" ~doc ~man in
Cmd.v info combined_term
+
let () = exit (Cmd.eval cmd)
+8 -1
stack/requests/lib/auth.ml
···
type t =
| None
| Basic of { username : string; password : string }
···
match auth with
| None -> headers
| Basic { username; password } ->
Headers.basic ~username ~password headers
| Bearer { token } ->
Headers.bearer token headers
-
| Digest { username = _; password = _ } ->
(* Digest auth requires server challenge first, handled elsewhere *)
headers
| Custom f ->
f headers
···
+
let src = Logs.Src.create "requests.auth" ~doc:"HTTP Authentication"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
type t =
| None
| Basic of { username : string; password : string }
···
match auth with
| None -> headers
| Basic { username; password } ->
+
Log.debug (fun m -> m "Applying basic authentication for user: %s" username);
Headers.basic ~username ~password headers
| Bearer { token } ->
+
Log.debug (fun m -> m "Applying bearer token authentication");
Headers.bearer token headers
+
| Digest { username; password = _ } ->
+
Log.debug (fun m -> m "Digest auth configured for user: %s (requires server challenge)" username);
(* Digest auth requires server challenge first, handled elsewhere *)
headers
| Custom f ->
+
Log.debug (fun m -> m "Applying custom authentication handler");
f headers
+3
stack/requests/lib/auth.mli
···
(** Authentication mechanisms *)
type t
(** Abstract authentication type *)
···
(** Authentication mechanisms *)
+
(** Log source for authentication operations *)
+
val src : Logs.Src.t
+
type t
(** Abstract authentication type *)
+14 -5
stack/requests/lib/body.ml
···
type 'a part = {
name : string;
filename : string option;
···
| None ->
(* Guess MIME type from filename if available *)
let path = Eio.Path.native_exn file in
-
if String.ends_with ~suffix:".json" path then Mime.json
-
else if String.ends_with ~suffix:".html" path then Mime.html
-
else if String.ends_with ~suffix:".xml" path then Mime.xml
-
else if String.ends_with ~suffix:".txt" path then Mime.text
-
else Mime.octet_stream
in
File { file; mime }
let json json_string =
···
+
let src = Logs.Src.create "requests.body" ~doc:"HTTP Request/Response Body"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
type 'a part = {
name : string;
filename : string option;
···
| None ->
(* Guess MIME type from filename if available *)
let path = Eio.Path.native_exn file in
+
let guessed =
+
if String.ends_with ~suffix:".json" path then Mime.json
+
else if String.ends_with ~suffix:".html" path then Mime.html
+
else if String.ends_with ~suffix:".xml" path then Mime.xml
+
else if String.ends_with ~suffix:".txt" path then Mime.text
+
else Mime.octet_stream
+
in
+
Log.debug (fun m -> m "Guessed MIME type %s for file %s" (Mime.to_string guessed) path);
+
guessed
in
+
Log.debug (fun m -> m "Creating file body from %s with MIME type %s"
+
(Eio.Path.native_exn file) (Mime.to_string mime));
File { file; mime }
let json json_string =
+3
stack/requests/lib/body.mli
···
]}
*)
type t
(** Abstract body type representing HTTP request body content. *)
···
]}
*)
+
(** Log source for body operations *)
+
val src : Logs.Src.t
+
type t
(** Abstract body type representing HTTP request body content. *)
-302
stack/requests/lib/client.ml
···
-
type ('a,'b) t = {
-
clock : 'a;
-
net : 'b;
-
default_headers : Headers.t;
-
timeout : Timeout.t;
-
max_retries : int;
-
retry_backoff : float;
-
verify_tls : bool;
-
tls_config : Tls.Config.client option;
-
}
-
-
let create
-
?(default_headers = Headers.empty)
-
?(timeout = Timeout.default)
-
?(max_retries = 3)
-
?(retry_backoff = 2.0)
-
?(verify_tls = true)
-
?tls_config
-
~clock
-
~net
-
() =
-
(* Create default TLS config if verify_tls is true and no custom config provided *)
-
let tls_config =
-
match tls_config, verify_tls with
-
| Some config, _ -> Some config
-
| None, true ->
-
(* Use CA certificates for verification *)
-
(match Ca_certs.authenticator () with
-
| Ok authenticator ->
-
(match Tls.Config.client ~authenticator () with
-
| Ok cfg -> Some cfg
-
| Error (`Msg msg) ->
-
Logs.warn (fun m -> m "Failed to create TLS config: %s" msg);
-
None)
-
| Error (`Msg msg) ->
-
Logs.warn (fun m -> m "Failed to load CA certificates: %s" msg);
-
None)
-
| None, false -> None
-
in
-
{
-
clock;
-
net;
-
default_headers;
-
timeout;
-
max_retries;
-
retry_backoff;
-
verify_tls;
-
tls_config;
-
}
-
-
let default ~clock ~net =
-
create ~clock ~net ()
-
-
(* Accessors *)
-
let clock t = t.clock
-
let net t = t.net
-
let default_headers t = t.default_headers
-
let timeout t = t.timeout
-
let max_retries t = t.max_retries
-
let retry_backoff t = t.retry_backoff
-
let verify_tls t = t.verify_tls
-
let tls_config t = t.tls_config
-
-
(* HTTP Request Methods *)
-
-
let src = Logs.Src.create "requests.client" ~doc:"HTTP Request Client"
-
module Log = (val Logs.src_log src : Logs.LOG)
-
-
(* Helper to get client or use default *)
-
let get_client client =
-
match client with
-
| Some c -> c
-
| None -> failwith "No client provided"
-
-
(* Convert our Headers.t to Cohttp.Header.t *)
-
let headers_to_cohttp headers =
-
Headers.to_list headers
-
|> Cohttp.Header.of_list
-
-
(* Convert Cohttp.Header.t to our Headers.t *)
-
let headers_from_cohttp cohttp_headers =
-
Cohttp.Header.to_list cohttp_headers
-
|> Headers.of_list
-
-
(* Main request implementation *)
-
let request ~sw ?client ?headers ?body ?auth ?timeout ?follow_redirects
-
?max_redirects ~method_ url =
-
let client = get_client client in
-
let start_time = Unix.gettimeofday () in
-
-
Log.info (fun m -> m "Making %s request to %s" (Method.to_string method_) url);
-
-
(* Prepare headers *)
-
let headers = match headers with
-
| Some h -> h
-
| None -> default_headers client
-
in
-
-
(* Apply auth *)
-
let headers = match auth with
-
| Some a ->
-
Log.debug (fun m -> m "Applying authentication");
-
Auth.apply a headers
-
| None -> headers
-
in
-
-
(* Add content type from body *)
-
let headers = match body with
-
| Some b -> (match Body.content_type b with
-
| Some mime -> Headers.content_type mime headers
-
| None -> headers)
-
| None -> headers
-
in
-
-
(* Convert to Cohttp types *)
-
let cohttp_method =
-
match Method.to_string method_ with
-
| "GET" -> `GET
-
| "POST" -> `POST
-
| "PUT" -> `PUT
-
| "DELETE" -> `DELETE
-
| "HEAD" -> `HEAD
-
| "OPTIONS" -> `OPTIONS
-
| "PATCH" -> `PATCH
-
| "CONNECT" -> `CONNECT
-
| "TRACE" -> `TRACE
-
| _ -> `GET
-
in
-
-
let cohttp_headers = headers_to_cohttp headers in
-
let cohttp_body = match body with
-
| Some b -> Body.Private.to_cohttp_body ~sw b
-
| None -> None
-
in
-
-
(* Make request using cohttp-eio *)
-
let uri = Uri.of_string url in
-
-
(* Create HTTPS handler if TLS is configured *)
-
let https = match tls_config client with
-
| None ->
-
Log.debug (fun m -> m "No TLS configuration");
-
None
-
| Some tls_config ->
-
Log.debug (fun m -> m "Using TLS configuration");
-
let https_fn uri socket =
-
let host =
-
Uri.host uri
-
|> Option.map (fun x -> Domain_name.(host_exn (of_string_exn x)))
-
in
-
Tls_eio.client_of_flow ?host tls_config socket
-
in
-
Some https_fn
-
in
-
-
(* Create the client *)
-
let eio_client = Cohttp_eio.Client.make ~https (net client) in
-
-
(* Apply timeout if specified *)
-
let make_request () =
-
Cohttp_eio.Client.call ~sw eio_client cohttp_method uri ~headers:cohttp_headers ?body:cohttp_body
-
in
-
-
(* Make the actual request with optional timeout *)
-
let resp, resp_body =
-
match timeout with
-
| Some t ->
-
let timeout_seconds = Timeout.total t in
-
(match timeout_seconds with
-
| Some seconds ->
-
Log.debug (fun m -> m "Setting timeout: %.2f seconds" seconds);
-
Eio.Time.with_timeout_exn (clock client) seconds make_request
-
| None -> make_request ())
-
| None -> make_request ()
-
in
-
-
let status = Cohttp.Response.status resp |> Cohttp.Code.code_of_status in
-
let cohttp_resp_headers = Cohttp.Response.headers resp in
-
let resp_headers = headers_from_cohttp cohttp_resp_headers in
-
-
Log.info (fun m -> m "Received response: status=%d" status);
-
-
(* Handle redirects if enabled *)
-
let follow_redirects = Option.value follow_redirects ~default:true in
-
let max_redirects = Option.value max_redirects ~default:10 in
-
-
let final_resp, final_body, final_url =
-
if follow_redirects && (status >= 300 && status < 400) then
-
let rec follow_redirect url redirects_left =
-
if redirects_left <= 0 then begin
-
Log.err (fun m -> m "Too many redirects (%d) for %s" max_redirects url);
-
raise (Error.TooManyRedirects { url; count = max_redirects; max = max_redirects })
-
end else
-
(* Get location header from Cohttp headers *)
-
match Cohttp.Header.get cohttp_resp_headers "location" with
-
| None ->
-
Log.debug (fun m -> m "Redirect response missing Location header");
-
(resp, resp_body, url)
-
| Some location ->
-
Log.info (fun m -> m "Following redirect to %s (%d remaining)"
-
location redirects_left);
-
(* Make new request to redirect location *)
-
let new_uri = Uri.of_string location in
-
let new_resp, new_body =
-
Cohttp_eio.Client.call ~sw eio_client cohttp_method new_uri ~headers:cohttp_headers
-
in
-
let new_status = Cohttp.Response.status new_resp |> Cohttp.Code.code_of_status in
-
if new_status >= 300 && new_status < 400 then
-
follow_redirect location (redirects_left - 1)
-
else
-
(new_resp, new_body, location)
-
in
-
follow_redirect url max_redirects
-
else
-
(resp, resp_body, url)
-
in
-
-
(* Get final headers *)
-
let final_headers =
-
if final_resp == resp then
-
resp_headers
-
else
-
Cohttp.Response.headers final_resp |> headers_from_cohttp
-
in
-
-
let elapsed = Unix.gettimeofday () -. start_time in
-
Log.info (fun m -> m "Request completed in %.3f seconds" elapsed);
-
-
Response.Private.make
-
~sw
-
~status
-
~headers:final_headers
-
~body:final_body
-
~url:final_url
-
~elapsed
-
-
(* Convenience methods *)
-
let get ~sw ?client ?headers ?auth ?timeout ?follow_redirects ?max_redirects url =
-
request ~sw ?client ?headers ?auth ?timeout ?follow_redirects ?max_redirects
-
~method_:`GET url
-
-
let post ~sw ?client ?headers ?body ?auth ?timeout url =
-
request ~sw ?client ?headers ?body ?auth ?timeout ~method_:`POST url
-
-
let put ~sw ?client ?headers ?body ?auth ?timeout url =
-
request ~sw ?client ?headers ?body ?auth ?timeout ~method_:`PUT url
-
-
let delete ~sw ?client ?headers ?auth ?timeout url =
-
request ~sw ?client ?headers ?auth ?timeout ~method_:`DELETE url
-
-
let head ~sw ?client ?headers ?auth ?timeout url =
-
request ~sw ?client ?headers ?auth ?timeout ~method_:`HEAD url
-
-
let patch ~sw ?client ?headers ?body ?auth ?timeout url =
-
request ~sw ?client ?headers ?body ?auth ?timeout ~method_:`PATCH url
-
-
let upload ~sw ?client ?headers ?auth ?timeout ?method_ ?mime ?length
-
?on_progress ~source url =
-
let method_ = Option.value method_ ~default:`POST in
-
let mime = Option.value mime ~default:Mime.octet_stream in
-
-
(* Wrap source with progress tracking if callback provided *)
-
let tracked_source = match on_progress with
-
| None -> source
-
| Some callback ->
-
(* For now, progress tracking is not implemented for uploads
-
due to complexity of wrapping Eio.Flow.source.
-
This would require creating a custom flow wrapper. *)
-
let _ = callback in
-
source
-
in
-
-
let body = Body.of_stream ?length mime tracked_source in
-
request ~sw ?client ?headers ~body ?auth ?timeout ~method_ url
-
-
let download ~sw ?client ?headers ?auth ?timeout ?on_progress url ~sink =
-
let response = get ~sw ?client ?headers ?auth ?timeout url in
-
-
try
-
(* Get content length for progress tracking *)
-
let total = Response.content_length response in
-
-
let body = Response.body response in
-
-
(* Stream data to sink with optional progress *)
-
match on_progress with
-
| None ->
-
(* No progress tracking, just copy directly *)
-
Eio.Flow.copy body sink
-
| Some progress_fn ->
-
(* Copy with progress tracking *)
-
(* We need to intercept the flow to track bytes *)
-
(* For now, just do a simple copy - proper progress tracking needs flow wrapper *)
-
progress_fn ~received:0L ~total;
-
Eio.Flow.copy body sink;
-
progress_fn ~received:(Option.value total ~default:0L) ~total;
-
-
(* Response auto-closes with switch *)
-
()
-
with e ->
-
(* Response auto-closes with switch *)
-
raise e
···
-202
stack/requests/lib/client.mli
···
-
(** Low-level HTTP client with streaming support
-
-
The Client module provides a stateless HTTP client with connection pooling,
-
TLS support, and streaming capabilities. For stateful requests with automatic
-
cookie handling and persistent configuration, use the {!Session} module instead.
-
-
{2 Examples}
-
-
{[
-
open Eio_main
-
-
let () = run @@ fun env ->
-
Switch.run @@ fun sw ->
-
-
(* Create a client *)
-
let client = Client.create ~clock:env#clock ~net:env#net () in
-
-
(* Simple GET request *)
-
let response = Client.get ~sw ~client "https://example.com" in
-
Printf.printf "Status: %d\n" (Response.status_code response);
-
Response.close response;
-
-
(* POST with JSON body *)
-
let response = Client.post ~sw ~client
-
~body:(Body.json {|{"key": "value"}|})
-
~headers:(Headers.empty |> Headers.content_type Mime.json)
-
"https://api.example.com/data" in
-
Response.close response;
-
-
(* Download file with streaming *)
-
Client.download ~sw ~client
-
"https://example.com/large-file.zip"
-
~sink:(Eio.Path.(fs / "download.zip" |> sink))
-
]}
-
*)
-
-
type ('a,'b) t
-
(** Client configuration with clock and network types.
-
The type parameters track the Eio environment capabilities. *)
-
-
(** {1 Client Creation} *)
-
-
val create :
-
?default_headers:Headers.t ->
-
?timeout:Timeout.t ->
-
?max_retries:int ->
-
?retry_backoff:float ->
-
?verify_tls:bool ->
-
?tls_config:Tls.Config.client ->
-
clock:'a Eio.Time.clock ->
-
net:'b Eio.Net.t ->
-
unit -> ('a Eio.Time.clock, 'b Eio.Net.t) t
-
(** [create ?default_headers ?timeout ?max_retries ?retry_backoff ?verify_tls ?tls_config ~clock ~net ()]
-
creates a new HTTP client with the specified configuration.
-
-
@param default_headers Headers to include in every request (default: empty)
-
@param timeout Default timeout configuration (default: 30s connect, 60s read)
-
@param max_retries Maximum number of retries for failed requests (default: 3)
-
@param retry_backoff Exponential backoff factor for retries (default: 2.0)
-
@param verify_tls Whether to verify TLS certificates (default: true)
-
@param tls_config Custom TLS configuration (default: uses system CA certificates)
-
@param clock Eio clock for timeouts and scheduling
-
@param net Eio network capability for making connections
-
*)
-
-
val default : clock:'a Eio.Time.clock -> net:'b Eio.Net.t -> ('a Eio.Time.clock, 'b Eio.Net.t) t
-
(** [default ~clock ~net] creates a client with default configuration.
-
Equivalent to [create ~clock ~net ()]. *)
-
-
(** {1 Configuration Access} *)
-
-
val clock : ('a,'b) t -> 'a
-
(** [clock client] returns the clock capability. *)
-
-
val net : ('a,'b) t -> 'b
-
(** [net client] returns the network capability. *)
-
-
val default_headers : ('a,'b) t -> Headers.t
-
(** [default_headers client] returns the default headers. *)
-
-
val timeout : ('a,'b) t -> Timeout.t
-
(** [timeout client] returns the timeout configuration. *)
-
-
val max_retries : ('a,'b) t -> int
-
(** [max_retries client] returns the maximum retry count. *)
-
-
val retry_backoff : ('a,'b) t -> float
-
(** [retry_backoff client] returns the retry backoff factor. *)
-
-
val verify_tls : ('a,'b) t -> bool
-
(** [verify_tls client] returns whether TLS verification is enabled. *)
-
-
val tls_config : ('a,'b) t -> Tls.Config.client option
-
(** [tls_config client] returns the TLS configuration if set. *)
-
-
(** {1 HTTP Request Methods} *)
-
-
val request :
-
sw:Eio.Switch.t ->
-
?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?body:Body.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
?follow_redirects:bool ->
-
?max_redirects:int ->
-
method_:Method.t ->
-
string ->
-
Response.t
-
(** Make a streaming request *)
-
-
val get :
-
sw:Eio.Switch.t ->
-
?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
?follow_redirects:bool ->
-
?max_redirects:int ->
-
string ->
-
Response.t
-
(** GET request *)
-
-
val post :
-
sw:Eio.Switch.t ->
-
?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?body:Body.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
string ->
-
Response.t
-
(** POST request *)
-
-
val put :
-
sw:Eio.Switch.t ->
-
?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?body:Body.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
string ->
-
Response.t
-
(** PUT request *)
-
-
val delete :
-
sw:Eio.Switch.t ->
-
?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
string ->
-
Response.t
-
(** DELETE request *)
-
-
val head :
-
sw:Eio.Switch.t ->
-
?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
string ->
-
Response.t
-
(** HEAD request *)
-
-
val patch :
-
sw:Eio.Switch.t ->
-
?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?body:Body.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
string ->
-
Response.t
-
(** PATCH request *)
-
-
val upload :
-
sw:Eio.Switch.t ->
-
?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
?method_:Method.t ->
-
?mime:Mime.t ->
-
?length:int64 ->
-
?on_progress:(sent:int64 -> total:int64 option -> unit) ->
-
source:Eio.Flow.source_ty Eio.Resource.t ->
-
string ->
-
Response.t
-
(** Upload from stream *)
-
-
val download :
-
sw:Eio.Switch.t ->
-
?client:(_ Eio.Time.clock , _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
?on_progress:(received:int64 -> total:int64 option -> unit) ->
-
string ->
-
sink:Eio.Flow.sink_ty Eio.Resource.t ->
-
unit
-
(** Download to stream *)
···
+3
stack/requests/lib/error.ml
···
(** Centralized error handling for the Requests library *)
(** {1 Exception Types} *)
exception Timeout
···
(** Centralized error handling for the Requests library *)
+
let src = Logs.Src.create "requests.error" ~doc:"HTTP Request Errors"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
(** {1 Exception Types} *)
exception Timeout
+3
stack/requests/lib/error.mli
···
(** Centralized error handling for the Requests library *)
(** {1 Exception Types} *)
(** Raised when a request times out *)
···
(** Centralized error handling for the Requests library *)
+
(** Log source for error reporting *)
+
val src : Logs.Src.t
+
(** {1 Exception Types} *)
(** Raised when a request times out *)
+3
stack/requests/lib/headers.mli
···
]}
*)
type t
(** Abstract header collection type. Headers are stored with case-insensitive
keys and maintain insertion order. *)
···
]}
*)
+
(** Log source for header operations *)
+
val src : Logs.Src.t
+
type t
(** Abstract header collection type. Headers are stored with case-insensitive
keys and maintain insertion order. *)
+3
stack/requests/lib/method.ml
···
type t = [
| `GET
| `POST
···
+
let src = Logs.Src.create "requests.method" ~doc:"HTTP Methods"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
type t = [
| `GET
| `POST
+3
stack/requests/lib/method.mli
···
(** HTTP methods following RFC 7231 *)
(** HTTP method type using polymorphic variants for better composability *)
type t = [
| `GET (** Retrieve a resource *)
···
(** HTTP methods following RFC 7231 *)
+
(** Log source for method operations *)
+
val src : Logs.Src.t
+
(** HTTP method type using polymorphic variants for better composability *)
type t = [
| `GET (** Retrieve a resource *)
+3
stack/requests/lib/mime.ml
···
type t = {
type_ : string;
subtype : string;
···
+
let src = Logs.Src.create "requests.mime" ~doc:"MIME Type Handling"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
type t = {
type_ : string;
subtype : string;
+3
stack/requests/lib/mime.mli
···
(** MIME type handling *)
type t
(** Abstract MIME type *)
···
(** MIME type handling *)
+
(** Log source for MIME type operations *)
+
val src : Logs.Src.t
+
type t
(** Abstract MIME type *)
+396 -3
stack/requests/lib/requests.ml
···
(** OCaml HTTP client library with streaming support *)
-
(* Re-export all modules *)
module Method = Method
module Mime = Mime
module Headers = Headers
···
module Timeout = Timeout
module Body = Body
module Response = Response
-
module Client = Client
module Status = Status
module Error = Error
-
module Session = Session
module Retry = Retry
···
(** OCaml HTTP client library with streaming support *)
+
let src = Logs.Src.create "requests" ~doc:"HTTP Client Library"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
module Method = Method
module Mime = Mime
module Headers = Headers
···
module Timeout = Timeout
module Body = Body
module Response = Response
+
module One = One
module Status = Status
module Error = Error
module Retry = Retry
+
+
(* Main API - Session functionality with concurrent fiber spawning *)
+
+
type ('clock, 'net) t = {
+
sw : Eio.Switch.t;
+
client : ('clock, 'net) One.t;
+
clock : 'clock;
+
cookie_jar : Cookeio.jar;
+
cookie_mutex : Eio.Mutex.t;
+
mutable default_headers : Headers.t;
+
mutable auth : Auth.t option;
+
mutable timeout : Timeout.t;
+
mutable follow_redirects : bool;
+
mutable max_redirects : int;
+
mutable retry : Retry.config option;
+
persist_cookies : bool;
+
xdg : Xdge.t option;
+
(* Statistics *)
+
mutable requests_made : int;
+
mutable total_time : float;
+
mutable retries_count : int;
+
}
+
+
let create
+
~sw
+
?client
+
?cookie_jar
+
?(default_headers = Headers.empty)
+
?auth
+
?(timeout = Timeout.default)
+
?(follow_redirects = true)
+
?(max_redirects = 10)
+
?(verify_tls = true)
+
?retry
+
?(persist_cookies = false)
+
?xdg
+
env =
+
+
let xdg = match xdg, persist_cookies with
+
| Some x, _ -> Some x
+
| None, true -> Some (Xdge.create env#fs "requests")
+
| None, false -> None
+
in
+
+
let client = match client with
+
| Some c -> c
+
| None ->
+
One.create ~verify_tls ~timeout
+
~clock:env#clock ~net:env#net ()
+
in
+
+
let cookie_jar = match cookie_jar, persist_cookies, xdg with
+
| Some jar, _, _ -> jar
+
| None, true, Some xdg_ctx ->
+
let data_dir = Xdge.data_dir xdg_ctx in
+
let cookie_file = Eio.Path.(data_dir / "cookies.txt") in
+
Cookeio.load cookie_file
+
| None, _, _ ->
+
Cookeio.create ()
+
in
+
+
{
+
sw;
+
client;
+
clock = env#clock;
+
cookie_jar;
+
cookie_mutex = Eio.Mutex.create ();
+
default_headers;
+
auth;
+
timeout;
+
follow_redirects;
+
max_redirects;
+
retry;
+
persist_cookies;
+
xdg;
+
requests_made = 0;
+
total_time = 0.0;
+
retries_count = 0;
+
}
+
+
(* Configuration management *)
+
let set_default_header t key value =
+
t.default_headers <- Headers.set key value t.default_headers
+
+
let remove_default_header t key =
+
t.default_headers <- Headers.remove key t.default_headers
+
+
let set_auth t auth =
+
Log.debug (fun m -> m "Setting authentication method");
+
t.auth <- Some auth
+
+
let clear_auth t =
+
Log.debug (fun m -> m "Clearing authentication");
+
t.auth <- None
+
+
let set_timeout t timeout =
+
Log.debug (fun m -> m "Setting timeout: %a" Timeout.pp timeout);
+
t.timeout <- timeout
+
+
let set_retry t config =
+
Log.debug (fun m -> m "Setting retry config: max_retries=%d" config.Retry.max_retries);
+
t.retry <- Some config
+
+
let cookies t = t.cookie_jar
+
let clear_cookies t = Cookeio.clear t.cookie_jar
+
+
(* Internal request function that runs in a fiber *)
+
let make_request_internal t ?headers ?body ?auth ?timeout ?follow_redirects ?max_redirects ~method_ url =
+
Log.info (fun m -> m "Making %s request to %s" (Method.to_string method_) url);
+
(* Merge headers *)
+
let headers = match headers with
+
| Some h -> Headers.merge t.default_headers h
+
| None -> t.default_headers
+
in
+
+
(* Use provided auth or default *)
+
let auth = match auth with
+
| Some a -> Some a
+
| None -> t.auth
+
in
+
+
(* Get cookies for this URL *)
+
let uri = Uri.of_string url in
+
let domain = Option.value ~default:"" (Uri.host uri) in
+
let path = Uri.path uri in
+
let is_secure = Uri.scheme uri = Some "https" in
+
+
let headers =
+
Eio.Mutex.use_ro t.cookie_mutex (fun () ->
+
let cookies = Cookeio.get_cookies t.cookie_jar ~domain ~path ~is_secure in
+
match cookies with
+
| [] -> headers
+
| cookies ->
+
Log.debug (fun m -> m "Adding %d cookies for %s%s" (List.length cookies) domain path);
+
let cookie_header = Cookeio.make_cookie_header cookies in
+
Headers.set "Cookie" cookie_header headers
+
)
+
in
+
+
(* Make the actual request *)
+
let response = One.request ~sw:t.sw ~client:t.client
+
?body ?auth
+
~timeout:(Option.value timeout ~default:t.timeout)
+
~follow_redirects:(Option.value follow_redirects ~default:t.follow_redirects)
+
~max_redirects:(Option.value max_redirects ~default:t.max_redirects)
+
~headers ~method_ url
+
in
+
+
(* Extract and store cookies from response *)
+
let () =
+
Eio.Mutex.use_rw ~protect:true t.cookie_mutex (fun () ->
+
match Response.headers response |> Headers.get_all "Set-Cookie" with
+
| [] -> ()
+
| cookie_headers ->
+
Log.debug (fun m -> m "Received %d Set-Cookie headers" (List.length cookie_headers));
+
List.iter (fun cookie_str ->
+
match Cookeio.parse_set_cookie ~domain ~path cookie_str with
+
| Some cookie ->
+
Log.debug (fun m -> m "Storing cookie");
+
Cookeio.add_cookie t.cookie_jar cookie
+
| None ->
+
Log.warn (fun m -> m "Failed to parse cookie: %s" cookie_str)
+
) cookie_headers
+
)
+
in
+
+
(* Update statistics *)
+
t.requests_made <- t.requests_made + 1;
+
Log.info (fun m -> m "Request completed with status %d" (Response.status_code response));
+
+
response
+
+
(* Public request function - executes synchronously *)
+
let request t ?headers ?body ?auth ?timeout ?follow_redirects ?max_redirects ~method_ url =
+
make_request_internal t ?headers ?body ?auth ?timeout
+
?follow_redirects ?max_redirects ~method_ url
+
+
(* Convenience methods *)
+
let get t ?headers ?auth ?timeout ?params url =
+
let url = match params with
+
| Some p ->
+
let uri = Uri.of_string url in
+
let uri = List.fold_left (fun u (k, v) -> Uri.add_query_param' u (k, v)) uri p in
+
Uri.to_string uri
+
| None -> url
+
in
+
request t ?headers ?auth ?timeout ~method_:`GET url
+
+
let post t ?headers ?body ?auth ?timeout url =
+
request t ?headers ?body ?auth ?timeout ~method_:`POST url
+
+
let put t ?headers ?body ?auth ?timeout url =
+
request t ?headers ?body ?auth ?timeout ~method_:`PUT url
+
+
let patch t ?headers ?body ?auth ?timeout url =
+
request t ?headers ?body ?auth ?timeout ~method_:`PATCH url
+
+
let delete t ?headers ?auth ?timeout url =
+
request t ?headers ?auth ?timeout ~method_:`DELETE url
+
+
let head t ?headers ?auth ?timeout url =
+
request t ?headers ?auth ?timeout ~method_:`HEAD url
+
+
let options t ?headers ?auth ?timeout url =
+
request t ?headers ?auth ?timeout ~method_:`OPTIONS url
+
+
(* Cmdliner integration module *)
+
module Cmd = struct
+
open Cmdliner
+
+
type config = {
+
xdg : Xdge.t * Xdge.Cmd.t;
+
persist_cookies : bool;
+
verify_tls : bool;
+
timeout : float option;
+
max_retries : int;
+
retry_backoff : float;
+
follow_redirects : bool;
+
max_redirects : int;
+
user_agent : string option;
+
}
+
+
let create config env sw =
+
let xdg, _xdg_cmd = config.xdg in
+
let retry = if config.max_retries > 0 then
+
Some (Retry.create_config
+
~max_retries:config.max_retries
+
~backoff_factor:config.retry_backoff ())
+
else None in
+
+
let timeout = match config.timeout with
+
| Some t -> Timeout.create ~total:t ()
+
| None -> Timeout.default in
+
+
let req = create ~sw
+
~xdg
+
~persist_cookies:config.persist_cookies
+
~verify_tls:config.verify_tls
+
~timeout
+
?retry
+
~follow_redirects:config.follow_redirects
+
~max_redirects:config.max_redirects
+
env in
+
+
(* Set user agent if provided *)
+
Option.iter (set_default_header req "User-Agent") config.user_agent;
+
+
req
+
+
(* Individual terms - parameterized by app_name *)
+
+
let persist_cookies_term app_name =
+
let doc = "Persist cookies to disk between sessions" in
+
let env_name = String.uppercase_ascii app_name ^ "_PERSIST_COOKIES" in
+
let env = Cmd.Env.info env_name in
+
Arg.(value & flag & info ["persist-cookies"] ~env ~doc)
+
+
let verify_tls_term app_name =
+
let doc = "Skip TLS certificate verification (insecure)" in
+
let env_name = String.uppercase_ascii app_name ^ "_NO_VERIFY_TLS" in
+
let env = Cmd.Env.info env_name in
+
Term.(const (fun no_verify -> not no_verify) $
+
Arg.(value & flag & info ["no-verify-tls"] ~env ~doc))
+
+
let timeout_term app_name =
+
let doc = "Request timeout in seconds" in
+
let env_name = String.uppercase_ascii app_name ^ "_TIMEOUT" in
+
let env = Cmd.Env.info env_name in
+
Arg.(value & opt (some float) None & info ["timeout"] ~env ~docv:"SECONDS" ~doc)
+
+
let retries_term app_name =
+
let doc = "Maximum number of request retries" in
+
let env_name = String.uppercase_ascii app_name ^ "_MAX_RETRIES" in
+
let env = Cmd.Env.info env_name in
+
Arg.(value & opt int 3 & info ["max-retries"] ~env ~docv:"N" ~doc)
+
+
let retry_backoff_term app_name =
+
let doc = "Retry backoff factor for exponential delay" in
+
let env_name = String.uppercase_ascii app_name ^ "_RETRY_BACKOFF" in
+
let env = Cmd.Env.info env_name in
+
Arg.(value & opt float 0.3 & info ["retry-backoff"] ~env ~docv:"FACTOR" ~doc)
+
+
let follow_redirects_term app_name =
+
let doc = "Don't follow HTTP redirects" in
+
let env_name = String.uppercase_ascii app_name ^ "_NO_FOLLOW_REDIRECTS" in
+
let env = Cmd.Env.info env_name in
+
Term.(const (fun no_follow -> not no_follow) $
+
Arg.(value & flag & info ["no-follow-redirects"] ~env ~doc))
+
+
let max_redirects_term app_name =
+
let doc = "Maximum number of redirects to follow" in
+
let env_name = String.uppercase_ascii app_name ^ "_MAX_REDIRECTS" in
+
let env = Cmd.Env.info env_name in
+
Arg.(value & opt int 10 & info ["max-redirects"] ~env ~docv:"N" ~doc)
+
+
let user_agent_term app_name =
+
let doc = "User-Agent header to send with requests" in
+
let env_name = String.uppercase_ascii app_name ^ "_USER_AGENT" in
+
let env = Cmd.Env.info env_name in
+
Arg.(value & opt (some string) None & info ["user-agent"] ~env ~docv:"STRING" ~doc)
+
+
(* Combined terms *)
+
+
let config_term app_name fs =
+
let xdg_term = Xdge.Cmd.term app_name fs
+
~config:true ~data:true ~cache:true ~state:false ~runtime:false () in
+
Term.(const (fun xdg persist verify timeout retries backoff follow max_redir ua ->
+
{ xdg; persist_cookies = persist; verify_tls = verify;
+
timeout; max_retries = retries; retry_backoff = backoff;
+
follow_redirects = follow; max_redirects = max_redir;
+
user_agent = ua })
+
$ xdg_term
+
$ persist_cookies_term app_name
+
$ verify_tls_term app_name
+
$ timeout_term app_name
+
$ retries_term app_name
+
$ retry_backoff_term app_name
+
$ follow_redirects_term app_name
+
$ max_redirects_term app_name
+
$ user_agent_term app_name)
+
+
let requests_term app_name env sw =
+
let config_t = config_term app_name env#fs in
+
Term.(const (fun config -> create config env sw) $ config_t)
+
+
let minimal_term app_name fs =
+
let xdg_term = Xdge.Cmd.term app_name fs
+
~config:false ~data:true ~cache:true ~state:false ~runtime:false () in
+
Term.(const (fun (xdg, _xdg_cmd) persist -> (xdg, persist))
+
$ xdg_term
+
$ persist_cookies_term app_name)
+
+
let env_docs app_name =
+
let app_upper = String.uppercase_ascii app_name in
+
Printf.sprintf
+
"## ENVIRONMENT\n\n\
+
The following environment variables affect %s:\n\n\
+
**%s_CONFIG_DIR**\n\
+
: Override configuration directory location\n\n\
+
**%s_DATA_DIR**\n\
+
: Override data directory location (for cookies)\n\n\
+
**%s_CACHE_DIR**\n\
+
: Override cache directory location\n\n\
+
**XDG_CONFIG_HOME**\n\
+
: Base directory for user configuration files (default: ~/.config)\n\n\
+
**XDG_DATA_HOME**\n\
+
: Base directory for user data files (default: ~/.local/share)\n\n\
+
**XDG_CACHE_HOME**\n\
+
: Base directory for user cache files (default: ~/.cache)\n\n\
+
**%s_PERSIST_COOKIES**\n\
+
: Set to '1' to persist cookies by default\n\n\
+
**%s_NO_VERIFY_TLS**\n\
+
: Set to '1' to disable TLS verification (insecure)\n\n\
+
**%s_TIMEOUT**\n\
+
: Default request timeout in seconds\n\n\
+
**%s_MAX_RETRIES**\n\
+
: Maximum number of retries (default: 3)\n\n\
+
**%s_RETRY_BACKOFF**\n\
+
: Retry backoff factor (default: 0.3)\n\n\
+
**%s_NO_FOLLOW_REDIRECTS**\n\
+
: Set to '1' to disable redirect following\n\n\
+
**%s_MAX_REDIRECTS**\n\
+
: Maximum redirects to follow (default: 10)\n\n\
+
**%s_USER_AGENT**\n\
+
: User-Agent header to send with requests\
+
"
+
app_name app_upper app_upper app_upper
+
app_upper app_upper app_upper app_upper
+
app_upper app_upper app_upper app_upper
+
+
let pp_config ppf config =
+
let _xdg, xdg_cmd = config.xdg in
+
Format.fprintf ppf "@[<v>Configuration:@,\
+
@[<v 2>XDG:@,%a@]@,\
+
persist_cookies: %b@,\
+
verify_tls: %b@,\
+
timeout: %a@,\
+
max_retries: %d@,\
+
retry_backoff: %.2f@,\
+
follow_redirects: %b@,\
+
max_redirects: %d@,\
+
user_agent: %a@]"
+
Xdge.Cmd.pp xdg_cmd
+
config.persist_cookies
+
config.verify_tls
+
(Format.pp_print_option Format.pp_print_float) config.timeout
+
config.max_retries
+
config.retry_backoff
+
config.follow_redirects
+
config.max_redirects
+
(Format.pp_print_option Format.pp_print_string) config.user_agent
+
end
+403 -36
stack/requests/lib/requests.mli
···
The Requests library offers two main ways to make HTTP requests:
-
{b 1. Session-based requests} (Recommended for most use cases)
-
Sessions maintain state across requests, handle cookies automatically,
-
and provide a simple interface for common tasks:
{[
open Eio_main
···
let () = run @@ fun env ->
Switch.run @@ fun sw ->
-
(* Create a session *)
-
let session = Requests.Session.create ~sw env in
(* Configure authentication once *)
-
Requests.Session.set_auth session (Requests.Auth.bearer "your-token");
-
(* Make requests - cookies and auth are handled automatically *)
-
let user = Requests.Session.get session "https://api.github.com/user" in
-
let repos = Requests.Session.get session "https://api.github.com/user/repos" in
-
(* Session automatically manages cookies *)
-
let _ = Requests.Session.post session "https://example.com/login"
-
~body:(Requests.Body.form ["username", "alice"; "password", "secret"]) in
-
let dashboard = Requests.Session.get session "https://example.com/dashboard"
(* No cleanup needed - responses auto-close with the switch *)
]}
-
{b 2. Client-based requests} (For fine-grained control)
-
The Client module provides lower-level control when you don't need
-
session state or want to manage connections manually:
{[
-
(* Create a client *)
-
let client = Requests.Client.create ~clock:env#clock ~net:env#net () in
(* Make a simple GET request *)
-
let response = Requests.Client.get ~sw ~client "https://api.github.com" in
Printf.printf "Status: %d\n" (Requests.Response.status_code response);
(* POST with custom headers and body *)
-
let response = Requests.Client.post ~sw ~client
~headers:(Requests.Headers.empty
|> Requests.Headers.content_type Requests.Mime.json
|> Requests.Headers.set "X-API-Key" "secret")
···
{2 Features}
- {b Simple API}: Intuitive functions for GET, POST, PUT, DELETE, etc.
-
- {b Sessions}: Maintain state (cookies, auth, headers) across requests
- {b Authentication}: Built-in support for Basic, Bearer, Digest, and OAuth
- {b Streaming}: Upload and download large files efficiently
- {b Retries}: Automatic retry with exponential backoff
···
{b Working with JSON APIs:}
{[
-
let response = Requests.Session.post session "https://api.example.com/data"
~body:(Requests.Body.json {|{"key": "value"}|}) in
let body_text =
Requests.Response.body response
···
content_type = Requests.Mime.text_plain;
content = `String "Important document" }
] in
-
let response = Requests.Session.post session "https://example.com/upload"
~body
(* Response auto-closes with switch *)
]}
{b Streaming downloads:}
{[
-
Requests.Client.download ~sw ~client
"https://example.com/large-file.zip"
~sink:(Eio.Path.(fs / "download.zip" |> sink))
]}
-
{2 Choosing Between Session and Client}
-
Use {b Session} when you need:
- Cookie persistence across requests
- Automatic retry handling
- Shared authentication across requests
- Request/response history tracking
- Configuration persistence to disk
-
Use {b Client} when you need:
- One-off stateless requests
- Fine-grained control over connections
- Minimal overhead
···
- Direct streaming without cookies
*)
-
(** {1 High-Level Session API}
-
Sessions provide stateful HTTP clients with automatic cookie management,
-
persistent configuration, and convenient methods for common operations.
*)
-
(** Stateful HTTP sessions with cookies and configuration persistence *)
-
module Session = Session
(** Retry policies and backoff strategies *)
module Retry = Retry
-
(** {1 Low-Level Client API}
-
The Client module provides direct control over HTTP requests without
session state. Use this for stateless operations or when you need
fine-grained control.
*)
-
(** Low-level HTTP client with connection pooling *)
-
module Client = Client
(** {1 Core Types}
···
(** Timeout configuration for requests *)
module Timeout = Timeout
···
The Requests library offers two main ways to make HTTP requests:
+
{b 1. Main API} (Recommended for most use cases)
+
The main API maintains state across requests, handles cookies automatically,
+
spawns requests in concurrent fibers, and provides a simple interface:
{[
open Eio_main
···
let () = run @@ fun env ->
Switch.run @@ fun sw ->
+
(* Create a requests instance *)
+
let req = Requests.create ~sw env in
(* Configure authentication once *)
+
Requests.set_auth req (Requests.Auth.bearer "your-token");
+
(* Make concurrent requests using Fiber.both *)
+
let (user, repos) = Eio.Fiber.both
+
(fun () -> Requests.get req "https://api.github.com/user")
+
(fun () -> Requests.get req "https://api.github.com/user/repos") in
+
(* Process responses *)
+
let user_data = Response.body user |> Eio.Flow.read_all in
+
let repos_data = Response.body repos |> Eio.Flow.read_all in
(* No cleanup needed - responses auto-close with the switch *)
]}
+
{b 2. One-shot requests} (For stateless operations)
+
The One module provides lower-level control for stateless,
+
one-off requests without session state:
{[
+
(* Create a one-shot client *)
+
let client = Requests.One.create ~clock:env#clock ~net:env#net () in
(* Make a simple GET request *)
+
let response = Requests.One.get ~sw ~client "https://api.github.com" in
Printf.printf "Status: %d\n" (Requests.Response.status_code response);
(* POST with custom headers and body *)
+
let response = Requests.One.post ~sw ~client
~headers:(Requests.Headers.empty
|> Requests.Headers.content_type Requests.Mime.json
|> Requests.Headers.set "X-API-Key" "secret")
···
{2 Features}
- {b Simple API}: Intuitive functions for GET, POST, PUT, DELETE, etc.
- {b Authentication}: Built-in support for Basic, Bearer, Digest, and OAuth
- {b Streaming}: Upload and download large files efficiently
- {b Retries}: Automatic retry with exponential backoff
···
{b Working with JSON APIs:}
{[
+
let response = Requests.post req "https://api.example.com/data"
~body:(Requests.Body.json {|{"key": "value"}|}) in
let body_text =
Requests.Response.body response
···
content_type = Requests.Mime.text_plain;
content = `String "Important document" }
] in
+
let response = Requests.post req "https://example.com/upload"
~body
(* Response auto-closes with switch *)
]}
{b Streaming downloads:}
{[
+
Requests.One.download ~sw ~client
"https://example.com/large-file.zip"
~sink:(Eio.Path.(fs / "download.zip" |> sink))
]}
+
{2 Choosing Between Main API and One}
+
Use the {b main API (Requests.t)} when you need:
- Cookie persistence across requests
- Automatic retry handling
- Shared authentication across requests
- Request/response history tracking
- Configuration persistence to disk
+
Use {b One} when you need:
- One-off stateless requests
- Fine-grained control over connections
- Minimal overhead
···
- Direct streaming without cookies
*)
+
(** {1 Main API}
+
The main Requests API provides stateful HTTP clients with automatic cookie
+
management and persistent configuration. Requests execute synchronously by default.
+
Use Eio.Fiber.both or Eio.Fiber.all for concurrent execution.
*)
+
type ('clock, 'net) t
+
(** A stateful HTTP client that maintains cookies, auth, and configuration across requests. *)
+
+
(** {2 Creation and Configuration} *)
+
+
val create :
+
sw:Eio.Switch.t ->
+
?client:('clock Eio.Time.clock,'net Eio.Net.t) One.t ->
+
?cookie_jar:Cookeio.jar ->
+
?default_headers:Headers.t ->
+
?auth:Auth.t ->
+
?timeout:Timeout.t ->
+
?follow_redirects:bool ->
+
?max_redirects:int ->
+
?verify_tls:bool ->
+
?retry:Retry.config ->
+
?persist_cookies:bool ->
+
?xdg:Xdge.t ->
+
< clock: 'clock Eio.Resource.t; net: 'net Eio.Resource.t; fs: Eio.Fs.dir_ty Eio.Path.t; .. > ->
+
('clock Eio.Resource.t, 'net Eio.Resource.t) t
+
(** Create a new requests instance with persistent state.
+
All resources are bound to the provided switch and will be cleaned up automatically. *)
+
+
(** {2 Configuration Management} *)
+
+
val set_default_header : ('clock, 'net) t -> string -> string -> unit
+
(** Set a default header for all requests *)
+
+
val remove_default_header : ('clock, 'net) t -> string -> unit
+
(** Remove a default header *)
+
+
val set_auth : ('clock, 'net) t -> Auth.t -> unit
+
(** Set default authentication *)
+
+
val clear_auth : ('clock, 'net) t -> unit
+
(** Clear authentication *)
+
+
val set_timeout : ('clock, 'net) t -> Timeout.t -> unit
+
(** Set default timeout *)
+
+
val set_retry : ('clock, 'net) t -> Retry.config -> unit
+
(** Set retry configuration *)
+
+
(** {2 Request Methods}
+
+
All request methods execute synchronously. To make concurrent requests,
+
you must explicitly use Eio.Fiber.both or Eio.Fiber.all.
+
The response will auto-close when the parent switch closes.
+
+
Example of concurrent requests using Fiber.both:
+
{[
+
let req = Requests.create ~sw env in
+
+
(* Use Fiber.both for two concurrent requests *)
+
let (r1, r2) = Eio.Fiber.both
+
(fun () -> Requests.get req "https://api1.example.com")
+
(fun () -> Requests.post req "https://api2.example.com" ~body)
+
in
+
+
(* Process responses *)
+
let body1 = Response.body r1 |> Eio.Flow.read_all in
+
let body2 = Response.body r2 |> Eio.Flow.read_all in
+
]}
+
+
Example using Fiber.all for multiple requests:
+
{[
+
let req = Requests.create ~sw env in
+
+
(* Use Fiber.all for multiple concurrent requests *)
+
let urls = [
+
"https://api1.example.com";
+
"https://api2.example.com";
+
"https://api3.example.com";
+
] in
+
+
let responses = ref [] in
+
Eio.Fiber.all [
+
(fun () -> responses := Requests.get req (List.nth urls 0) :: !responses);
+
(fun () -> responses := Requests.get req (List.nth urls 1) :: !responses);
+
(fun () -> responses := Requests.get req (List.nth urls 2) :: !responses);
+
];
+
+
(* Process all responses *)
+
List.iter (fun r ->
+
let body = Response.body r |> Eio.Flow.read_all in
+
print_endline body
+
) !responses
+
]}
+
+
Example using Promise for concurrent requests with individual control:
+
{[
+
let req = Requests.create ~sw env in
+
+
(* Start requests in parallel using promises *)
+
let p1, r1 = Eio.Promise.create () in
+
let p2, r2 = Eio.Promise.create () in
+
let p3, r3 = Eio.Promise.create () in
+
+
Eio.Fiber.fork ~sw (fun () ->
+
Eio.Promise.resolve r1 (Requests.get req "https://api1.example.com")
+
);
+
Eio.Fiber.fork ~sw (fun () ->
+
Eio.Promise.resolve r2 (Requests.post req "https://api2.example.com" ~body)
+
);
+
Eio.Fiber.fork ~sw (fun () ->
+
Eio.Promise.resolve r3 (Requests.get req "https://api3.example.com")
+
);
+
+
(* Wait for all promises and process *)
+
let resp1 = Eio.Promise.await p1 in
+
let resp2 = Eio.Promise.await p2 in
+
let resp3 = Eio.Promise.await p3 in
+
+
(* Process responses *)
+
let body1 = Response.body resp1 |> Eio.Flow.read_all in
+
let body2 = Response.body resp2 |> Eio.Flow.read_all in
+
let body3 = Response.body resp3 |> Eio.Flow.read_all in
+
]}
+
*)
+
+
val request :
+
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
?headers:Headers.t ->
+
?body:Body.t ->
+
?auth:Auth.t ->
+
?timeout:Timeout.t ->
+
?follow_redirects:bool ->
+
?max_redirects:int ->
+
method_:Method.t ->
+
string ->
+
Response.t
+
(** Make a concurrent HTTP request *)
+
+
val get :
+
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
?headers:Headers.t ->
+
?auth:Auth.t ->
+
?timeout:Timeout.t ->
+
?params:(string * string) list ->
+
string ->
+
Response.t
+
(** Concurrent GET request *)
+
+
val post :
+
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
?headers:Headers.t ->
+
?body:Body.t ->
+
?auth:Auth.t ->
+
?timeout:Timeout.t ->
+
string ->
+
Response.t
+
(** Concurrent POST request *)
+
+
val put :
+
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
?headers:Headers.t ->
+
?body:Body.t ->
+
?auth:Auth.t ->
+
?timeout:Timeout.t ->
+
string ->
+
Response.t
+
(** Concurrent PUT request *)
+
+
val patch :
+
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
?headers:Headers.t ->
+
?body:Body.t ->
+
?auth:Auth.t ->
+
?timeout:Timeout.t ->
+
string ->
+
Response.t
+
(** Concurrent PATCH request *)
+
+
val delete :
+
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
?headers:Headers.t ->
+
?auth:Auth.t ->
+
?timeout:Timeout.t ->
+
string ->
+
Response.t
+
(** Concurrent DELETE request *)
+
+
val head :
+
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
?headers:Headers.t ->
+
?auth:Auth.t ->
+
?timeout:Timeout.t ->
+
string ->
+
Response.t
+
(** Concurrent HEAD request *)
+
+
val options :
+
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
?headers:Headers.t ->
+
?auth:Auth.t ->
+
?timeout:Timeout.t ->
+
string ->
+
Response.t
+
(** Concurrent OPTIONS request *)
+
+
(** {2 Cookie Management} *)
+
+
val cookies : ('clock, 'net) t -> Cookeio.jar
+
(** Get the cookie jar for direct manipulation *)
+
+
val clear_cookies : ('clock, 'net) t -> unit
+
(** Clear all cookies *)
+
+
(** {1 Cmdliner Integration} *)
+
+
module Cmd : sig
+
(** Cmdliner integration for Requests configuration.
+
+
This module provides command-line argument handling for configuring
+
HTTP requests, including XDG directory paths, timeouts, retries,
+
and other parameters. *)
+
+
(** Configuration from command line and environment *)
+
type config = {
+
xdg : Xdge.t * Xdge.Cmd.t; (** XDG paths and their sources *)
+
persist_cookies : bool; (** Whether to persist cookies *)
+
verify_tls : bool; (** Whether to verify TLS certificates *)
+
timeout : float option; (** Request timeout in seconds *)
+
max_retries : int; (** Maximum number of retries *)
+
retry_backoff : float; (** Retry backoff factor *)
+
follow_redirects : bool; (** Whether to follow redirects *)
+
max_redirects : int; (** Maximum number of redirects *)
+
user_agent : string option; (** User-Agent header *)
+
}
+
+
val create : config -> < clock: ([> float Eio.Time.clock_ty ] as 'clock) Eio.Resource.t; net: ([> [>`Generic] Eio.Net.ty ] as 'net) Eio.Resource.t; fs: Eio.Fs.dir_ty Eio.Path.t; .. > -> Eio.Switch.t -> ('clock Eio.Resource.t, 'net Eio.Resource.t) t
+
(** [create config env sw] creates a requests instance from command-line configuration *)
+
+
(** {2 Individual Terms} *)
+
+
val persist_cookies_term : string -> bool Cmdliner.Term.t
+
(** Term for [--persist-cookies] flag with app-specific env var *)
+
+
val verify_tls_term : string -> bool Cmdliner.Term.t
+
(** Term for [--no-verify-tls] flag with app-specific env var *)
+
+
val timeout_term : string -> float option Cmdliner.Term.t
+
(** Term for [--timeout SECONDS] option with app-specific env var *)
+
+
val retries_term : string -> int Cmdliner.Term.t
+
(** Term for [--max-retries N] option with app-specific env var *)
+
+
val retry_backoff_term : string -> float Cmdliner.Term.t
+
(** Term for [--retry-backoff FACTOR] option with app-specific env var *)
+
+
val follow_redirects_term : string -> bool Cmdliner.Term.t
+
(** Term for [--no-follow-redirects] flag with app-specific env var *)
+
+
val max_redirects_term : string -> int Cmdliner.Term.t
+
(** Term for [--max-redirects N] option with app-specific env var *)
+
+
val user_agent_term : string -> string option Cmdliner.Term.t
+
(** Term for [--user-agent STRING] option with app-specific env var *)
+
+
(** {2 Combined Terms} *)
+
+
val config_term : string -> Eio.Fs.dir_ty Eio.Path.t -> config Cmdliner.Term.t
+
(** [config_term app_name fs] creates a complete configuration term.
+
+
This combines all individual terms plus XDG configuration into
+
a single term that can be used to configure requests.
+
+
{b Generated Flags:}
+
- [--config-dir DIR]: Configuration directory
+
- [--data-dir DIR]: Data directory
+
- [--cache-dir DIR]: Cache directory
+
- [--persist-cookies]: Enable cookie persistence
+
- [--no-verify-tls]: Disable TLS verification
+
- [--timeout SECONDS]: Request timeout
+
- [--max-retries N]: Maximum retries
+
- [--retry-backoff FACTOR]: Retry backoff multiplier
+
- [--no-follow-redirects]: Disable redirect following
+
- [--max-redirects N]: Maximum redirects to follow
+
- [--user-agent STRING]: User-Agent header
+
+
{b Example:}
+
{[
+
let open Cmdliner in
+
let config_t = Requests.Cmd.config_term "myapp" env#fs in
+
let main config =
+
Eio.Switch.run @@ fun sw ->
+
let req = Requests.Cmd.create config env sw in
+
(* Use requests *)
+
in
+
let cmd = Cmd.v info Term.(const main $ config_t) in
+
Cmd.eval cmd
+
]} *)
+
+
val requests_term : string -> < clock: ([> float Eio.Time.clock_ty ] as 'clock) Eio.Resource.t; net: ([> [>`Generic] Eio.Net.ty ] as 'net) Eio.Resource.t; fs: Eio.Fs.dir_ty Eio.Path.t; .. > -> Eio.Switch.t -> ('clock Eio.Resource.t, 'net Eio.Resource.t) t Cmdliner.Term.t
+
(** [requests_term app_name env sw] creates a term that directly produces a requests instance.
+
+
This is a convenience function that combines configuration parsing
+
with requests creation.
+
+
{b Example:}
+
{[
+
let open Cmdliner in
+
let main req =
+
(* Use requests directly *)
+
let resp = Requests.get req "https://example.com" in
+
(* ... *)
+
in
+
Eio.Switch.run @@ fun sw ->
+
let req_t = Requests.Cmd.requests_term "myapp" env sw in
+
let cmd = Cmd.v info Term.(const main $ req_t) in
+
Cmd.eval cmd
+
]} *)
+
+
val minimal_term : string -> Eio.Fs.dir_ty Eio.Path.t -> (Xdge.t * bool) Cmdliner.Term.t
+
(** [minimal_term app_name fs] creates a minimal configuration term.
+
+
This only provides:
+
- [--cache-dir DIR]: Cache directory for responses
+
- [--persist-cookies]: Cookie persistence flag
+
+
Returns the XDG context and persist_cookies boolean.
+
+
{b Example:}
+
{[
+
let open Cmdliner in
+
let minimal_t = Requests.Cmd.minimal_term "myapp" env#fs in
+
let main (xdg, persist) =
+
Eio.Switch.run @@ fun sw ->
+
let req = Requests.create ~sw ~xdg ~persist_cookies:persist env in
+
(* Use requests *)
+
in
+
let cmd = Cmd.v info Term.(const main $ minimal_t) in
+
Cmd.eval cmd
+
]} *)
+
+
(** {2 Documentation} *)
+
+
val env_docs : string -> string
+
(** [env_docs app_name] generates environment variable documentation.
+
+
Returns formatted documentation for all environment variables that
+
affect requests configuration, including XDG variables.
+
+
{b Included Variables:}
+
- [${APP_NAME}_CONFIG_DIR]: Configuration directory
+
- [${APP_NAME}_DATA_DIR]: Data directory
+
- [${APP_NAME}_CACHE_DIR]: Cache directory
+
- [${APP_NAME}_STATE_DIR]: State directory
+
- [XDG_CONFIG_HOME], [XDG_DATA_HOME], [XDG_CACHE_HOME], [XDG_STATE_HOME]
+
- [HTTP_PROXY], [HTTPS_PROXY], [NO_PROXY] (when proxy support is added)
+
+
{b Example:}
+
{[
+
let env_info = Cmdliner.Cmd.Env.info
+
~docs:Cmdliner.Manpage.s_environment
+
~doc:(Requests.Cmd.env_docs "myapp")
+
()
+
]} *)
+
+
val pp_config : Format.formatter -> config -> unit
+
(** Pretty print configuration for debugging *)
+
end
(** Retry policies and backoff strategies *)
module Retry = Retry
+
(** {1 One-Shot API}
+
The One module provides direct control over HTTP requests without
session state. Use this for stateless operations or when you need
fine-grained control.
*)
+
(** One-shot HTTP client for stateless requests *)
+
module One = One
(** {1 Core Types}
···
(** Timeout configuration for requests *)
module Timeout = Timeout
+
+
(** {2 Logging} *)
+
+
(** Log source for the requests library.
+
Use [Logs.Src.set_level src] to control logging verbosity.
+
Example: [Logs.Src.set_level Requests.src (Some Logs.Debug)] *)
+
val src : Logs.Src.t
+3
stack/requests/lib/response.mli
···
open Eio
type t
(** Abstract response type representing an HTTP response. *)
···
open Eio
+
(** Log source for response operations *)
+
val src : Logs.Src.t
+
type t
(** Abstract response type representing an HTTP response. *)
+3
stack/requests/lib/retry.mli
···
open Eio
(** Retry configuration *)
type config = {
max_retries : int; (** Maximum number of retry attempts *)
···
open Eio
+
(** Log source for retry operations *)
+
val src : Logs.Src.t
+
(** Retry configuration *)
type config = {
max_retries : int; (** Maximum number of retry attempts *)
-550
stack/requests/lib/session.ml
···
-
let src = Logs.Src.create "requests.session" ~doc:"HTTP Session"
-
module Log = (val Logs.src_log src : Logs.LOG)
-
-
(** {1 Types} *)
-
-
(** Session statistics *)
-
module Stats = struct
-
type t = {
-
requests_made : int;
-
total_time : float;
-
cookies_count : int;
-
retries_count : int;
-
}
-
-
let requests_made t = t.requests_made
-
let total_time t = t.total_time
-
let cookies_count t = t.cookies_count
-
let retries_count t = t.retries_count
-
-
let pp ppf t =
-
Format.fprintf ppf "@[<v>Session Statistics:@,\
-
requests made: %d@,\
-
total time: %.3fs@,\
-
cookies: %d@,\
-
retries: %d@]"
-
t.requests_made
-
t.total_time
-
t.cookies_count
-
t.retries_count
-
end
-
-
type ('clock, 'net) t = {
-
sw : Eio.Switch.t;
-
client : ('clock, 'net) Client.t;
-
clock : 'clock;
-
cookie_jar : Cookeio.jar;
-
mutable default_headers : Headers.t;
-
mutable auth : Auth.t option;
-
mutable timeout : Timeout.t;
-
mutable follow_redirects : bool;
-
mutable max_redirects : int;
-
mutable retry : Retry.config option;
-
persist_cookies : bool;
-
xdg : Xdge.t option;
-
(* Statistics *)
-
mutable requests_made : int;
-
mutable total_time : float;
-
mutable retries_count : int;
-
mutex : Mutex.t;
-
}
-
-
(** {1 Session Creation} *)
-
-
let create
-
~sw
-
?client
-
?cookie_jar
-
?(default_headers = Headers.empty)
-
?auth
-
?(timeout = Timeout.default)
-
?(follow_redirects = true)
-
?(max_redirects = 10)
-
?(verify_tls = true)
-
?retry
-
?(persist_cookies = false)
-
?xdg
-
env =
-
-
(* Create default XDG context if needed *)
-
let xdg = match xdg, persist_cookies with
-
| Some x, _ -> Some x
-
| None, true -> Some (Xdge.create env#fs "requests")
-
| None, false -> None
-
in
-
-
Log.info (fun m -> m "Creating new session%s"
-
(match xdg with
-
| Some x -> Printf.sprintf " with XDG app=%s" (Xdge.app_name x)
-
| None -> ""));
-
-
(* Create or use provided client *)
-
let client = match client with
-
| Some c -> c
-
| None ->
-
Client.create ~verify_tls ~timeout
-
~clock:env#clock ~net:env#net ()
-
in
-
-
(* Create or load cookie jar *)
-
let cookie_jar = match cookie_jar, persist_cookies, xdg with
-
| Some jar, _, _ -> jar
-
| None, true, Some xdg_ctx ->
-
Log.debug (fun m -> m "Loading persistent cookie jar from XDG data dir");
-
let data_dir = Xdge.data_dir xdg_ctx in
-
let cookie_file = Eio.Path.(data_dir / "cookies.txt") in
-
Cookeio.load cookie_file
-
| None, _, _ ->
-
Cookeio.create ()
-
in
-
-
let session = {
-
sw;
-
client;
-
clock = env#clock;
-
cookie_jar;
-
default_headers;
-
auth;
-
timeout;
-
follow_redirects;
-
max_redirects;
-
retry;
-
persist_cookies;
-
xdg;
-
requests_made = 0;
-
total_time = 0.0;
-
retries_count = 0;
-
mutex = Mutex.create ();
-
} in
-
-
(* Register cleanup on switch *)
-
Eio.Switch.on_release sw (fun () ->
-
Log.info (fun m -> m "Closing session after %d requests" session.requests_made);
-
if persist_cookies && Option.is_some xdg then begin
-
Log.info (fun m -> m "Saving cookies on session close");
-
let data_dir = Xdge.data_dir (Option.get xdg) in
-
let cookie_file = Eio.Path.(data_dir / "cookies.txt") in
-
Cookeio.save cookie_file session.cookie_jar
-
end
-
);
-
-
session
-
-
let save_cookies : ('a, 'b) t -> unit = fun t ->
-
if t.persist_cookies && Option.is_some t.xdg then
-
let data_dir = Xdge.data_dir (Option.get t.xdg) in
-
let cookie_file = Eio.Path.(data_dir / "cookies.txt") in
-
Cookeio.save cookie_file t.cookie_jar
-
-
let load_cookies : ('a, 'b) t -> unit = fun t ->
-
if t.persist_cookies && Option.is_some t.xdg then
-
let data_dir = Xdge.data_dir (Option.get t.xdg) in
-
let cookie_file = Eio.Path.(data_dir / "cookies.txt") in
-
let loaded = Cookeio.load cookie_file in
-
(* Copy loaded cookies into our jar *)
-
Cookeio.clear t.cookie_jar;
-
let cookies_from_loaded = Cookeio.to_mozilla_format loaded in
-
let _reloaded = Cookeio.from_mozilla_format cookies_from_loaded in
-
(* This is a bit convoluted but maintains the same jar reference *)
-
()
-
-
(** {1 Configuration Management} *)
-
-
let set_default_header t key value =
-
t.default_headers <- Headers.set key value t.default_headers;
-
Log.debug (fun m -> m "Set default header %s: %s" key value)
-
-
let remove_default_header t key =
-
t.default_headers <- Headers.remove key t.default_headers;
-
Log.debug (fun m -> m "Removed default header %s" key)
-
-
let set_auth t auth =
-
t.auth <- Some auth;
-
Log.debug (fun m -> m "Set session authentication")
-
-
let clear_auth t =
-
t.auth <- None;
-
Log.debug (fun m -> m "Cleared session authentication")
-
-
let set_timeout t timeout =
-
t.timeout <- timeout
-
-
let set_retry t retry =
-
t.retry <- Some retry
-
-
let disable_retry t =
-
t.retry <- None
-
-
(** {1 Cookie Management} *)
-
-
let cookies t = t.cookie_jar
-
-
let clear_cookies t =
-
Cookeio.clear t.cookie_jar
-
-
(** {1 Internal Request Function} *)
-
-
let execute_request t ?headers ?body ?auth ?timeout ?follow_redirects ?max_redirects ~method_ url =
-
let start_time = Unix.gettimeofday () in
-
-
(* Merge headers: default -> cookie -> provided *)
-
let headers =
-
t.default_headers
-
|> Headers.merge (Option.value headers ~default:Headers.empty)
-
|> (fun headers ->
-
let uri = Uri.of_string url in
-
let domain = Uri.host_with_default ~default:"localhost" uri in
-
let path = Uri.path uri in
-
let is_secure = Uri.scheme uri = Some "https" in
-
let cookies = Cookeio.get_cookies t.cookie_jar ~domain ~path ~is_secure in
-
if cookies = [] then headers
-
else
-
let cookie_header = Cookeio.make_cookie_header cookies in
-
Headers.add "cookie" cookie_header headers)
-
in
-
-
(* Use provided auth or session default *)
-
let auth = match auth with Some a -> Some a | None -> t.auth in
-
-
(* Use provided or session defaults *)
-
let timeout = Option.value timeout ~default:t.timeout in
-
let follow_redirects = Option.value follow_redirects ~default:t.follow_redirects in
-
let max_redirects = Option.value max_redirects ~default:t.max_redirects in
-
-
Log.info (fun m -> m "Session request: %s %s"
-
(Method.to_string method_) url);
-
-
(* Make the actual request with retry if configured *)
-
let make_request () =
-
(* Use Client.request to make the actual HTTP request *)
-
Client.request ~sw:t.sw ~client:t.client
-
~headers ~method_ ?body ?auth ~timeout
-
~follow_redirects ~max_redirects url
-
in
-
-
let response = match t.retry with
-
| None -> make_request ()
-
| Some retry_config ->
-
Retry.with_retry ~sw:t.sw ~clock:t.clock
-
~config:retry_config
-
~f:make_request
-
~should_retry_exn:(function
-
(* Retry on retryable errors *)
-
| Error.Timeout -> true
-
| Error.ConnectionError _ -> true
-
| Error.HTTPError { status; _ } when status >= 500 -> true (* Server errors *)
-
| Error.SSLError _ -> false (* Don't retry SSL errors *)
-
| Error.ProxyError _ -> true
-
| Eio.Io (Eio.Net.E (Connection_reset _), _) -> true
-
| Eio.Time.Timeout -> true
-
| _ -> false)
-
in
-
-
(* Extract cookies from response *)
-
let uri = Uri.of_string url in
-
let domain = Uri.host_with_default ~default:"localhost" uri in
-
let path =
-
let p = Uri.path uri in
-
if p = "" then "/"
-
else
-
let last_slash = String.rindex_opt p '/' in
-
match last_slash with
-
| None -> "/"
-
| Some i -> String.sub p 0 (i + 1)
-
in
-
let set_cookie_values = Headers.get_multi "set-cookie" (Response.headers response) in
-
List.iter (fun value ->
-
match Cookeio.parse_set_cookie ~domain ~path value with
-
| Some cookie -> Cookeio.add_cookie t.cookie_jar cookie
-
| None -> Log.warn (fun m -> m "Failed to parse Set-Cookie header: %s" value)
-
) set_cookie_values;
-
-
(* Update statistics *)
-
Mutex.lock t.mutex;
-
t.requests_made <- t.requests_made + 1;
-
t.total_time <- t.total_time +. (Unix.gettimeofday () -. start_time);
-
Mutex.unlock t.mutex;
-
-
response
-
-
(** {1 Request Methods} *)
-
-
let request t ?headers ?body ?auth ?timeout ?follow_redirects ?max_redirects ~method_ url =
-
execute_request t ?headers ?body ?auth ?timeout ?follow_redirects ?max_redirects ~method_ url
-
-
let get t ?headers ?auth ?timeout ?params url =
-
let url = match params with
-
| None -> url
-
| Some params ->
-
let uri = Uri.of_string url in
-
let uri = List.fold_left (fun u (k, v) ->
-
Uri.add_query_param' u (k, v)
-
) uri params in
-
Uri.to_string uri
-
in
-
execute_request t ?headers ?auth ?timeout ~method_:`GET url
-
-
let post t ?headers ?body ?auth ?timeout url =
-
execute_request t ?headers ?body ?auth ?timeout ~method_:`POST url
-
-
let put t ?headers ?body ?auth ?timeout url =
-
execute_request t ?headers ?body ?auth ?timeout ~method_:`PUT url
-
-
let patch t ?headers ?body ?auth ?timeout url =
-
execute_request t ?headers ?body ?auth ?timeout ~method_:`PATCH url
-
-
let delete t ?headers ?auth ?timeout url =
-
execute_request t ?headers ?auth ?timeout ~method_:`DELETE url
-
-
let head t ?headers ?auth ?timeout url =
-
execute_request t ?headers ?auth ?timeout ~method_:`HEAD url
-
-
let options t ?headers ?auth ?timeout url =
-
execute_request t ?headers ?auth ?timeout ~method_:`OPTIONS url
-
-
(** {1 Streaming Operations} *)
-
-
let upload t ?headers ?auth ?timeout ?method_ ?mime ?length ~source url =
-
let method_ = Option.value method_ ~default:`POST in
-
let body = Body.of_stream ?length (Option.value mime ~default:Mime.octet_stream) source in
-
(* Progress tracking would require wrapping Eio.Flow.source which is complex.
-
Use Client.upload with on_progress callback for progress tracking instead. *)
-
execute_request t ?headers ~body ?auth ?timeout ~method_ url
-
-
let download t ?headers ?auth ?timeout url ~sink =
-
let response = execute_request t ?headers ?auth ?timeout ~method_:`GET url in
-
let body = Response.body response in
-
(* Progress tracking would require intercepting Eio.Flow.copy.
-
Use Client.download with on_progress callback for progress tracking instead. *)
-
Eio.Flow.copy body sink
-
-
let download_file t ?headers ?auth ?timeout url path =
-
Eio.Path.with_open_out ~create:(`Or_truncate 0o644) path @@ fun sink ->
-
download t ?headers ?auth ?timeout url ~sink
-
-
-
let pp ppf t =
-
Mutex.lock t.mutex;
-
let stats = t.requests_made, t.total_time,
-
Cookeio.count t.cookie_jar in
-
Mutex.unlock t.mutex;
-
let requests, time, cookies = stats in
-
Format.fprintf ppf "@[<v>Session:@,\
-
requests made: %d@,\
-
total time: %.3fs@,\
-
cookies: %d@,\
-
auth: %s@,\
-
follow redirects: %b@,\
-
max redirects: %d@,\
-
retry: %s@,\
-
persist cookies: %b@,\
-
XDG: %s@]"
-
requests time cookies
-
(if Option.is_some t.auth then "configured" else "none")
-
t.follow_redirects
-
t.max_redirects
-
(if Option.is_some t.retry then "enabled" else "disabled")
-
t.persist_cookies
-
(match t.xdg with
-
| Some x -> Xdge.app_name x
-
| None -> "none")
-
-
let stats t =
-
Mutex.lock t.mutex;
-
let result = Stats.{
-
requests_made = t.requests_made;
-
total_time = t.total_time;
-
cookies_count = Cookeio.count t.cookie_jar;
-
retries_count = t.retries_count;
-
} in
-
Mutex.unlock t.mutex;
-
result
-
-
(** {1 Cmdliner Integration} *)
-
-
module Cmd = struct
-
open Cmdliner
-
-
type config = {
-
xdg : Xdge.t * Xdge.Cmd.t;
-
persist_cookies : bool;
-
verify_tls : bool;
-
timeout : float option;
-
max_retries : int;
-
retry_backoff : float;
-
follow_redirects : bool;
-
max_redirects : int;
-
user_agent : string option;
-
}
-
-
let create config env sw =
-
let xdg, _xdg_cmd = config.xdg in
-
let retry = if config.max_retries > 0 then
-
Some (Retry.create_config
-
~max_retries:config.max_retries
-
~backoff_factor:config.retry_backoff ())
-
else None in
-
-
let timeout = match config.timeout with
-
| Some t -> Timeout.create ~total:t ()
-
| None -> Timeout.default in
-
-
let session = create ~sw
-
~xdg
-
~persist_cookies:config.persist_cookies
-
~verify_tls:config.verify_tls
-
~timeout
-
?retry
-
~follow_redirects:config.follow_redirects
-
~max_redirects:config.max_redirects
-
env in
-
-
(* Set user agent if provided *)
-
Option.iter (set_default_header session "User-Agent") config.user_agent;
-
-
session
-
-
(* Individual terms - parameterized by app_name *)
-
-
let persist_cookies_term app_name =
-
let doc = "Persist cookies to disk between sessions" in
-
let env_name = String.uppercase_ascii app_name ^ "_PERSIST_COOKIES" in
-
let env = Cmd.Env.info env_name in
-
Arg.(value & flag & info ["persist-cookies"] ~env ~doc)
-
-
let verify_tls_term app_name =
-
let doc = "Skip TLS certificate verification (insecure)" in
-
let env_name = String.uppercase_ascii app_name ^ "_NO_VERIFY_TLS" in
-
let env = Cmd.Env.info env_name in
-
Term.(const (fun no_verify -> not no_verify) $
-
Arg.(value & flag & info ["no-verify-tls"] ~env ~doc))
-
-
let timeout_term app_name =
-
let doc = "Request timeout in seconds" in
-
let env_name = String.uppercase_ascii app_name ^ "_TIMEOUT" in
-
let env = Cmd.Env.info env_name in
-
Arg.(value & opt (some float) None & info ["timeout"] ~env ~docv:"SECONDS" ~doc)
-
-
let retries_term app_name =
-
let doc = "Maximum number of request retries" in
-
let env_name = String.uppercase_ascii app_name ^ "_MAX_RETRIES" in
-
let env = Cmd.Env.info env_name in
-
Arg.(value & opt int 3 & info ["max-retries"] ~env ~docv:"N" ~doc)
-
-
let retry_backoff_term app_name =
-
let doc = "Retry backoff factor for exponential delay" in
-
let env_name = String.uppercase_ascii app_name ^ "_RETRY_BACKOFF" in
-
let env = Cmd.Env.info env_name in
-
Arg.(value & opt float 0.3 & info ["retry-backoff"] ~env ~docv:"FACTOR" ~doc)
-
-
let follow_redirects_term app_name =
-
let doc = "Don't follow HTTP redirects" in
-
let env_name = String.uppercase_ascii app_name ^ "_NO_FOLLOW_REDIRECTS" in
-
let env = Cmd.Env.info env_name in
-
Term.(const (fun no_follow -> not no_follow) $
-
Arg.(value & flag & info ["no-follow-redirects"] ~env ~doc))
-
-
let max_redirects_term app_name =
-
let doc = "Maximum number of redirects to follow" in
-
let env_name = String.uppercase_ascii app_name ^ "_MAX_REDIRECTS" in
-
let env = Cmd.Env.info env_name in
-
Arg.(value & opt int 10 & info ["max-redirects"] ~env ~docv:"N" ~doc)
-
-
let user_agent_term app_name =
-
let doc = "User-Agent header to send with requests" in
-
let env_name = String.uppercase_ascii app_name ^ "_USER_AGENT" in
-
let env = Cmd.Env.info env_name in
-
Arg.(value & opt (some string) None & info ["user-agent"] ~env ~docv:"STRING" ~doc)
-
-
(* Combined terms *)
-
-
let config_term app_name fs =
-
let xdg_term = Xdge.Cmd.term app_name fs
-
~config:true ~data:true ~cache:true ~state:false ~runtime:false () in
-
Term.(const (fun xdg persist verify timeout retries backoff follow max_redir ua ->
-
{ xdg; persist_cookies = persist; verify_tls = verify;
-
timeout; max_retries = retries; retry_backoff = backoff;
-
follow_redirects = follow; max_redirects = max_redir;
-
user_agent = ua })
-
$ xdg_term
-
$ persist_cookies_term app_name
-
$ verify_tls_term app_name
-
$ timeout_term app_name
-
$ retries_term app_name
-
$ retry_backoff_term app_name
-
$ follow_redirects_term app_name
-
$ max_redirects_term app_name
-
$ user_agent_term app_name)
-
-
let session_term app_name env sw =
-
let config_t = config_term app_name env#fs in
-
Term.(const (fun config -> create config env sw) $ config_t)
-
-
let minimal_term app_name fs =
-
let xdg_term = Xdge.Cmd.term app_name fs
-
~config:false ~data:true ~cache:true ~state:false ~runtime:false () in
-
Term.(const (fun (xdg, _xdg_cmd) persist -> (xdg, persist))
-
$ xdg_term
-
$ persist_cookies_term app_name)
-
-
let env_docs app_name =
-
let app_upper = String.uppercase_ascii app_name in
-
Printf.sprintf
-
"## ENVIRONMENT\n\n\
-
The following environment variables affect %s:\n\n\
-
**%s_CONFIG_DIR**\n\
-
: Override configuration directory location\n\n\
-
**%s_DATA_DIR**\n\
-
: Override data directory location (for cookies)\n\n\
-
**%s_CACHE_DIR**\n\
-
: Override cache directory location\n\n\
-
**XDG_CONFIG_HOME**\n\
-
: Base directory for user configuration files (default: ~/.config)\n\n\
-
**XDG_DATA_HOME**\n\
-
: Base directory for user data files (default: ~/.local/share)\n\n\
-
**XDG_CACHE_HOME**\n\
-
: Base directory for user cache files (default: ~/.cache)\n\n\
-
**%s_PERSIST_COOKIES**\n\
-
: Set to '1' to persist cookies by default\n\n\
-
**%s_NO_VERIFY_TLS**\n\
-
: Set to '1' to disable TLS verification (insecure)\n\n\
-
**%s_TIMEOUT**\n\
-
: Default request timeout in seconds\n\n\
-
**%s_MAX_RETRIES**\n\
-
: Maximum number of retries for failed requests\n\n\
-
**%s_MAX_REDIRECTS**\n\
-
: Maximum number of redirects to follow\n\n\
-
**%s_NO_FOLLOW_REDIRECTS**\n\
-
: Set to '1' to disable following redirects\n\n\
-
**%s_RETRY_BACKOFF**\n\
-
: Backoff factor for retry delays\n\n\
-
**%s_USER_AGENT**\n\
-
: Default User-Agent header\n\n\
-
**HTTP_PROXY**, **HTTPS_PROXY**, **NO_PROXY**\n\
-
: Proxy configuration (when proxy support is implemented)"
-
app_name app_upper app_upper app_upper
-
app_upper app_upper app_upper app_upper
-
app_upper app_upper app_upper app_upper
-
-
let pp_config ppf config =
-
let _xdg, xdg_cmd = config.xdg in
-
Format.fprintf ppf "@[<v>Session Configuration:@,\
-
@[<v 2>XDG Directories:@,%a@]@,\
-
persist cookies: %b@,\
-
verify TLS: %b@,\
-
timeout: %s@,\
-
max retries: %d@,\
-
retry backoff: %.2f@,\
-
follow redirects: %b@,\
-
max redirects: %d@,\
-
user agent: %s@]"
-
Xdge.Cmd.pp xdg_cmd
-
config.persist_cookies
-
config.verify_tls
-
(match config.timeout with None -> "none" | Some t -> Printf.sprintf "%.1fs" t)
-
config.max_retries
-
config.retry_backoff
-
config.follow_redirects
-
config.max_redirects
-
(Option.value config.user_agent ~default:"default")
-
end
···
-461
stack/requests/lib/session.mli
···
-
(** HTTP Session with persistent state across requests
-
-
Sessions provide stateful HTTP clients that maintain cookies, default headers,
-
authentication, and other configuration across multiple requests. They follow
-
the Eio concurrency model and are thread-safe.
-
-
Example usage:
-
{[
-
Eio_main.run @@ fun env ->
-
Eio.Switch.run @@ fun sw ->
-
-
(* Create a session *)
-
let session = Session.create ~sw env in
-
-
(* Cookies are automatically handled *)
-
let resp1 = Session.get session "https://httpbin.org/cookies/set?foo=bar" in
-
let resp2 = Session.get session "https://httpbin.org/cookies" in
-
(* resp2 will include the foo=bar cookie *)
-
-
(* Set default headers for all requests *)
-
Session.set_default_header session "User-Agent" "MyApp/1.0";
-
-
(* Set authentication for all requests *)
-
Session.set_auth session (Auth.bearer "token123");
-
]}
-
*)
-
-
(** {1 Types} *)
-
-
type ('clock, 'net) t
-
(** A session maintains state across multiple HTTP requests *)
-
-
(** Session statistics *)
-
module Stats : sig
-
type t = {
-
requests_made : int; (** Total number of requests made *)
-
total_time : float; (** Total time spent in requests (seconds) *)
-
cookies_count : int; (** Number of cookies in the jar *)
-
retries_count : int; (** Total number of retries performed *)
-
}
-
-
(** Get the number of requests made *)
-
val requests_made : t -> int
-
-
(** Get the total time spent in requests *)
-
val total_time : t -> float
-
-
(** Get the number of cookies *)
-
val cookies_count : t -> int
-
-
(** Get the number of retries *)
-
val retries_count : t -> int
-
-
(** Pretty printer for statistics *)
-
val pp : Format.formatter -> t -> unit
-
end
-
-
(** {1 Session Creation and Configuration} *)
-
-
val create :
-
sw:Eio.Switch.t ->
-
?client:('clock Eio.Time.clock,'net Eio.Net.t) Client.t ->
-
?cookie_jar:Cookeio.jar ->
-
?default_headers:Headers.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
?follow_redirects:bool ->
-
?max_redirects:int ->
-
?verify_tls:bool ->
-
?retry:Retry.config ->
-
?persist_cookies:bool ->
-
?xdg:Xdge.t ->
-
< clock: 'clock Eio.Resource.t; net: 'net Eio.Resource.t; fs: Eio.Fs.dir_ty Eio.Path.t; .. > ->
-
('clock Eio.Resource.t, 'net Eio.Resource.t) t
-
(** Create a new session.
-
-
@param sw Switch for resource management
-
@param client Base client configuration (creates default if not provided)
-
@param cookie_jar Use existing cookie jar (creates new one if not provided)
-
@param default_headers Headers to include in all requests
-
@param auth Default authentication for all requests
-
@param timeout Default timeout for all requests
-
@param follow_redirects Whether to follow redirects (default: true)
-
@param max_redirects Maximum number of redirects (default: 10)
-
@param verify_tls Whether to verify TLS certificates (default: true)
-
@param retry Retry configuration for failed requests
-
@param persist_cookies Whether to save/load cookies from disk (default: false)
-
@param xdg XDG directory configuration (creates default "requests" if not provided)
-
*)
-
-
(** {1 Configuration Management} *)
-
-
val set_default_header : ('clock, 'net) t -> string -> string -> unit
-
(** Set a default header that will be included in all requests *)
-
-
val remove_default_header : ('clock, 'net) t -> string -> unit
-
(** Remove a default header *)
-
-
val set_auth : ('clock, 'net) t -> Auth.t -> unit
-
(** Set default authentication for all requests *)
-
-
val clear_auth : ('clock, 'net) t -> unit
-
(** Clear default authentication *)
-
-
val set_timeout : ('clock, 'net) t -> Timeout.t -> unit
-
(** Set default timeout for all requests *)
-
-
val set_retry : ('clock, 'net) t -> Retry.config -> unit
-
(** Set retry configuration *)
-
-
val disable_retry : ('clock, 'net) t -> unit
-
(** Disable automatic retry *)
-
-
(** {1 Cookie Management} *)
-
-
val cookies : ('clock, 'net) t -> Cookeio.jar
-
(** Get the session's cookie jar for direct manipulation *)
-
-
val clear_cookies : ('clock, 'net) t -> unit
-
(** Clear all cookies *)
-
-
val save_cookies : ('clock, 'net) t -> unit
-
(** Manually save cookies to disk (if persist_cookies was enabled) *)
-
-
val load_cookies : ('clock, 'net) t -> unit
-
(** Manually reload cookies from disk (if persist_cookies was enabled) *)
-
-
(** {1 Request Methods} *)
-
-
(** All request methods automatically:
-
- Include session's default headers
-
- Use session's authentication
-
- Handle cookies (extract from responses, add to requests)
-
- Apply retry logic if configured
-
- Follow redirects based on session configuration
-
*)
-
-
val request :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?body:Body.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
?follow_redirects:bool ->
-
?max_redirects:int ->
-
method_:Method.t ->
-
string ->
-
Response.t
-
(** Make a request with the session.
-
Optional parameters override session defaults. *)
-
-
val get :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
?params:(string * string) list ->
-
string ->
-
Response.t
-
(** GET request with optional query parameters *)
-
-
val post :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?body:Body.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
string ->
-
Response.t
-
(** POST request with optional body *)
-
-
val put :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?body:Body.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
string ->
-
Response.t
-
(** PUT request with optional body *)
-
-
val patch :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?body:Body.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
string ->
-
Response.t
-
(** PATCH request with optional body *)
-
-
val delete :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
string ->
-
Response.t
-
(** DELETE request *)
-
-
val head :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
string ->
-
Response.t
-
(** HEAD request *)
-
-
val options :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
string ->
-
Response.t
-
(** OPTIONS request *)
-
-
(** {1 Streaming Operations} *)
-
-
val upload :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
?method_:Method.t ->
-
?mime:Mime.t ->
-
?length:int64 ->
-
source:Eio.Flow.source_ty Eio.Resource.t ->
-
string ->
-
Response.t
-
(** Upload from a stream with optional progress callback *)
-
-
val download :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
string ->
-
sink:Eio.Flow.sink_ty Eio.Resource.t ->
-
unit
-
(** Download to a stream with optional progress callback *)
-
-
val download_file :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
-
?headers:Headers.t ->
-
?auth:Auth.t ->
-
?timeout:Timeout.t ->
-
string ->
-
_ Eio.Path.t ->
-
unit
-
(** Download directly to a file *)
-
-
val pp : Format.formatter -> ('clock, 'net) t -> unit
-
(** Pretty print session configuration *)
-
-
val stats : ('clock, 'net) t -> Stats.t
-
(** Get session statistics *)
-
-
(** {1 Examples} *)
-
-
(** {2 Basic Usage}
-
{[
-
let session = Session.create ~sw env in
-
let response = Session.get session "https://api.github.com/user" in
-
Printf.printf "Status: %d\n" (Response.status response)
-
]}
-
*)
-
-
(** {2 Posting JSON Data}
-
{[
-
let session = Session.create ~sw env in
-
let json_str = {|{"name": "John", "age": 30}|} in
-
let response = Session.post session
-
~body:(Body.json json_str)
-
"https://api.example.com/users" in
-
(* Response handling *)
-
]}
-
*)
-
-
(** {2 With Authentication}
-
{[
-
let session = Session.create ~sw env in
-
Session.set_auth session (Auth.bearer "github_token");
-
Session.set_default_header session "Accept" "application/vnd.github.v3+json";
-
-
let user = Session.get session "https://api.github.com/user" in
-
let repos = Session.get session "https://api.github.com/user/repos" in
-
(* Both requests will use the same auth token and headers *)
-
]}
-
*)
-
-
(** {2 Form Login with Cookies}
-
{[
-
let session = Session.create ~sw ~persist_cookies:true env in
-
-
(* Login - cookies will be saved *)
-
let login = Session.post session
-
~body:(Body.form ["username", "user"; "password", "pass"])
-
"https://example.com/login" in
-
-
(* Access protected resource - cookies are automatically included *)
-
let dashboard = Session.get session "https://example.com/dashboard" in
-
]}
-
*)
-
-
(** {1 Cmdliner Integration} *)
-
-
module Cmd : sig
-
(** Cmdliner integration for Requests session configuration.
-
-
This module provides command-line argument handling for configuring
-
HTTP sessions, including XDG directory paths, timeouts, retries,
-
and other session parameters. *)
-
-
(** Session configuration from command line and environment *)
-
type config = {
-
xdg : Xdge.t * Xdge.Cmd.t; (** XDG paths and their sources *)
-
persist_cookies : bool; (** Whether to persist cookies *)
-
verify_tls : bool; (** Whether to verify TLS certificates *)
-
timeout : float option; (** Request timeout in seconds *)
-
max_retries : int; (** Maximum number of retries *)
-
retry_backoff : float; (** Retry backoff factor *)
-
follow_redirects : bool; (** Whether to follow redirects *)
-
max_redirects : int; (** Maximum number of redirects *)
-
user_agent : string option; (** User-Agent header *)
-
}
-
-
val create : config -> < clock: ([> float Eio.Time.clock_ty ] as 'clock) Eio.Resource.t; net: ([> [>`Generic] Eio.Net.ty ] as 'net) Eio.Resource.t; fs: Eio.Fs.dir_ty Eio.Path.t; .. > -> Eio.Switch.t -> ('clock Eio.Resource.t, 'net Eio.Resource.t) t
-
(** [create config env sw] creates a session from command-line configuration *)
-
-
(** {2 Individual Terms} *)
-
-
val persist_cookies_term : string -> bool Cmdliner.Term.t
-
(** Term for [--persist-cookies] flag with app-specific env var *)
-
-
val verify_tls_term : string -> bool Cmdliner.Term.t
-
(** Term for [--no-verify-tls] flag with app-specific env var *)
-
-
val timeout_term : string -> float option Cmdliner.Term.t
-
(** Term for [--timeout SECONDS] option with app-specific env var *)
-
-
val retries_term : string -> int Cmdliner.Term.t
-
(** Term for [--max-retries N] option with app-specific env var *)
-
-
val retry_backoff_term : string -> float Cmdliner.Term.t
-
(** Term for [--retry-backoff FACTOR] option with app-specific env var *)
-
-
val follow_redirects_term : string -> bool Cmdliner.Term.t
-
(** Term for [--no-follow-redirects] flag with app-specific env var *)
-
-
val max_redirects_term : string -> int Cmdliner.Term.t
-
(** Term for [--max-redirects N] option with app-specific env var *)
-
-
val user_agent_term : string -> string option Cmdliner.Term.t
-
(** Term for [--user-agent STRING] option with app-specific env var *)
-
-
(** {2 Combined Terms} *)
-
-
val config_term : string -> Eio.Fs.dir_ty Eio.Path.t -> config Cmdliner.Term.t
-
(** [config_term app_name fs] creates a complete configuration term.
-
-
This combines all individual terms plus XDG configuration into
-
a single term that can be used to configure a session.
-
-
{b Generated Flags:}
-
- [--config-dir DIR]: Configuration directory
-
- [--data-dir DIR]: Data directory
-
- [--cache-dir DIR]: Cache directory
-
- [--state-dir DIR]: State directory
-
- [--persist-cookies]: Enable cookie persistence
-
- [--no-verify-tls]: Disable TLS verification
-
- [--timeout SECONDS]: Request timeout
-
- [--max-retries N]: Maximum retries
-
- [--retry-backoff FACTOR]: Retry backoff multiplier
-
- [--no-follow-redirects]: Disable redirect following
-
- [--max-redirects N]: Maximum redirects to follow
-
- [--user-agent STRING]: User-Agent header
-
-
{b Example:}
-
{[
-
let open Cmdliner in
-
let config_t = Session.Cmd.config_term "myapp" env#fs in
-
let main config =
-
Eio.Switch.run @@ fun sw ->
-
let session = Session.Cmd.create config env sw in
-
(* Use session *)
-
in
-
let cmd = Cmd.v info Term.(const main $ config_t) in
-
Cmd.eval cmd
-
]} *)
-
-
val session_term : string -> < clock: ([> float Eio.Time.clock_ty ] as 'clock) Eio.Resource.t; net: ([> [>`Generic] Eio.Net.ty ] as 'net) Eio.Resource.t; fs: Eio.Fs.dir_ty Eio.Path.t; .. > -> Eio.Switch.t -> ('clock Eio.Resource.t, 'net Eio.Resource.t) t Cmdliner.Term.t
-
(** [session_term app_name env sw] creates a term that directly produces a session.
-
-
This is a convenience function that combines configuration parsing
-
with session creation.
-
-
{b Example:}
-
{[
-
let open Cmdliner in
-
let main session =
-
(* Use session directly *)
-
let resp = Session.get session "https://example.com" in
-
(* ... *)
-
in
-
Eio.Switch.run @@ fun sw ->
-
let session_t = Session.Cmd.session_term "myapp" env sw in
-
let cmd = Cmd.v info Term.(const main $ session_t) in
-
Cmd.eval cmd
-
]} *)
-
-
val minimal_term : string -> Eio.Fs.dir_ty Eio.Path.t -> (Xdge.t * bool) Cmdliner.Term.t
-
(** [minimal_term app_name fs] creates a minimal configuration term.
-
-
This only provides:
-
- [--cache-dir DIR]: Cache directory for responses
-
- [--persist-cookies]: Cookie persistence flag
-
-
Returns the XDG context and persist_cookies boolean.
-
-
{b Example:}
-
{[
-
let open Cmdliner in
-
let minimal_t = Session.Cmd.minimal_term "myapp" env#fs in
-
let main (xdg, persist) =
-
Eio.Switch.run @@ fun sw ->
-
let session = Session.create ~sw ~xdg ~persist_cookies:persist env in
-
(* Use session *)
-
in
-
let cmd = Cmd.v info Term.(const main $ minimal_t) in
-
Cmd.eval cmd
-
]} *)
-
-
(** {2 Documentation} *)
-
-
val env_docs : string -> string
-
(** [env_docs app_name] generates environment variable documentation.
-
-
Returns formatted documentation for all environment variables that
-
affect session configuration, including XDG variables.
-
-
{b Included Variables:}
-
- [${APP_NAME}_CONFIG_DIR]: Configuration directory
-
- [${APP_NAME}_DATA_DIR]: Data directory
-
- [${APP_NAME}_CACHE_DIR]: Cache directory
-
- [${APP_NAME}_STATE_DIR]: State directory
-
- [XDG_CONFIG_HOME], [XDG_DATA_HOME], [XDG_CACHE_HOME], [XDG_STATE_HOME]
-
- [HTTP_PROXY], [HTTPS_PROXY], [NO_PROXY] (when proxy support is added)
-
-
{b Example:}
-
{[
-
let env_info = Cmdliner.Cmd.Env.info
-
~docs:Cmdliner.Manpage.s_environment
-
~doc:(Session.Cmd.env_docs "myapp")
-
()
-
]} *)
-
-
val pp_config : Format.formatter -> config -> unit
-
(** Pretty print session configuration for debugging *)
-
end
···
+3
stack/requests/lib/status.ml
···
(** HTTP status codes following RFC 7231 and extensions *)
type informational = [
| `Continue
| `Switching_protocols
···
(** HTTP status codes following RFC 7231 and extensions *)
+
let src = Logs.Src.create "requests.status" ~doc:"HTTP Status Codes"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
type informational = [
| `Continue
| `Switching_protocols
+3
stack/requests/lib/status.mli
···
(** HTTP status codes following RFC 7231 and extensions *)
(** {1 Status Categories} *)
type informational = [
···
(** HTTP status codes following RFC 7231 and extensions *)
+
(** Log source for status code operations *)
+
val src : Logs.Src.t
+
(** {1 Status Categories} *)
type informational = [
+3
stack/requests/lib/timeout.ml
···
type t = {
connect : float option;
read : float option;
···
+
let src = Logs.Src.create "requests.timeout" ~doc:"HTTP Request Timeouts"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
type t = {
connect : float option;
read : float option;
+3
stack/requests/lib/timeout.mli
···
(** Timeout configuration *)
type t
(** Timeout configuration *)
···
(** Timeout configuration *)
+
(** Log source for timeout operations *)
+
val src : Logs.Src.t
+
type t
(** Timeout configuration *)