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

more

+1 -2
stack/requests/bin/ocurl.ml
···
else
print_string body_str;
-
(* Close response to free resources *)
-
Requests.Response.close response;
+
(* Response auto-closes with switch *)
if not quiet && Requests.Response.ok response then
Logs.app (fun m -> m "✓ Success")
+118 -19
stack/requests/lib/body.ml
···
(* Complex to calculate, handled during sending *)
None
-
let to_cohttp_body = function
+
(* Strings_source - A flow source that streams from a doubly-linked list of strings/flows *)
+
module Strings_source = struct
+
type element =
+
| String of string
+
| Flow of Eio.Flow.source_ty Eio.Resource.t
+
+
type t = {
+
dllist : element Lwt_dllist.t;
+
mutable current_element : element option;
+
mutable string_offset : int;
+
}
+
+
let rec single_read t dst =
+
match t.current_element with
+
| None ->
+
(* Try to get the first element from the list *)
+
if Lwt_dllist.is_empty t.dllist then
+
raise End_of_file
+
else begin
+
t.current_element <- Some (Lwt_dllist.take_l t.dllist);
+
single_read t dst
+
end
+
| Some (String s) when t.string_offset >= String.length s ->
+
(* Current string exhausted, move to next element *)
+
t.current_element <- None;
+
t.string_offset <- 0;
+
single_read t dst
+
| Some (String s) ->
+
(* Read from current string *)
+
let available = String.length s - t.string_offset in
+
let to_read = min (Cstruct.length dst) available in
+
Cstruct.blit_from_string s t.string_offset dst 0 to_read;
+
t.string_offset <- t.string_offset + to_read;
+
to_read
+
| Some (Flow flow) ->
+
(* Read from flow *)
+
(try
+
let n = Eio.Flow.single_read flow dst in
+
if n = 0 then begin
+
(* Flow exhausted, move to next element *)
+
t.current_element <- None;
+
single_read t dst
+
end else n
+
with End_of_file ->
+
t.current_element <- None;
+
single_read t dst)
+
+
let read_methods = [] (* No special read methods *)
+
+
let create () = {
+
dllist = Lwt_dllist.create ();
+
current_element = None;
+
string_offset = 0;
+
}
+
+
let add_string t s =
+
ignore (Lwt_dllist.add_r (String s) t.dllist)
+
+
let add_flow t flow =
+
ignore (Lwt_dllist.add_r (Flow flow) t.dllist)
+
end
+
+
let strings_source_create () =
+
let t = Strings_source.create () in
+
let ops = Eio.Flow.Pi.source (module Strings_source) in
+
(t, Eio.Resource.T (t, ops))
+
+
let to_cohttp_body ~sw = function
| Empty -> None
| String { content; _ } -> Some (Cohttp_eio.Body.of_string content)
| Stream { source; _ } -> Some source
| File { file; _ } ->
-
(* Read file content *)
-
let content = Eio.Path.load file in
-
Some (Cohttp_eio.Body.of_string content)
+
(* Open file and stream it directly without loading into memory *)
+
let flow = Eio.Path.open_in ~sw file in
+
Some (flow :> Eio.Flow.source_ty Eio.Resource.t)
| Multipart { parts; boundary } ->
-
(* Create multipart body *)
-
let buffer = Buffer.create 1024 in
+
(* Create a single strings_source with dllist for streaming *)
+
let source, flow = strings_source_create () in
+
List.iter (fun part ->
-
Buffer.add_string buffer (Printf.sprintf "--%s\r\n" boundary);
-
Buffer.add_string buffer (Printf.sprintf "Content-Disposition: form-data; name=\"%s\"" part.name);
+
(* Add boundary *)
+
Strings_source.add_string source "--";
+
Strings_source.add_string source boundary;
+
Strings_source.add_string source "\r\n";
+
+
(* Add Content-Disposition header *)
+
Strings_source.add_string source "Content-Disposition: form-data; name=\"";
+
Strings_source.add_string source part.name;
+
Strings_source.add_string source "\"";
(match part.filename with
-
| Some f -> Buffer.add_string buffer (Printf.sprintf "; filename=\"%s\"" f)
+
| Some f ->
+
Strings_source.add_string source "; filename=\"";
+
Strings_source.add_string source f;
+
Strings_source.add_string source "\""
| None -> ());
-
Buffer.add_string buffer "\r\n";
-
Buffer.add_string buffer (Printf.sprintf "Content-Type: %s\r\n\r\n" (Mime.to_string part.content_type));
+
Strings_source.add_string source "\r\n";
+
+
(* Add Content-Type header *)
+
Strings_source.add_string source "Content-Type: ";
+
Strings_source.add_string source (Mime.to_string part.content_type);
+
Strings_source.add_string source "\r\n\r\n";
+
+
(* Add content *)
(match part.content with
-
| `String s -> Buffer.add_string buffer s
+
| `String s ->
+
Strings_source.add_string source s
| `File file ->
-
(* Read file content for multipart *)
-
let content = Eio.Path.load file in
-
Buffer.add_string buffer content
-
| `Stream _ -> ()); (* TODO: read stream *)
-
Buffer.add_string buffer "\r\n"
+
(* Open file and add as flow *)
+
let file_flow = Eio.Path.open_in ~sw file in
+
Strings_source.add_flow source (file_flow :> Eio.Flow.source_ty Eio.Resource.t)
+
| `Stream stream ->
+
(* Add stream directly *)
+
Strings_source.add_flow source stream);
+
+
(* Add trailing newline *)
+
Strings_source.add_string source "\r\n"
) parts;
-
Buffer.add_string buffer (Printf.sprintf "--%s--\r\n" boundary);
-
Some (Cohttp_eio.Body.of_string (Buffer.contents buffer))
+
+
(* Add final boundary *)
+
Strings_source.add_string source "--";
+
Strings_source.add_string source boundary;
+
Strings_source.add_string source "--\r\n";
+
+
Some flow
+
+
(* Private module *)
+
module Private = struct
+
let to_cohttp_body = to_cohttp_body
+
end
+88 -22
stack/requests/lib/body.mli
···
-
(** Request body construction *)
+
(** HTTP request body construction
+
+
This module provides various ways to construct HTTP request bodies,
+
including strings, files, streams, forms, and multipart data.
+
+
{2 Examples}
+
+
{[
+
(* Simple text body *)
+
let body = Body.text "Hello, World!"
+
+
(* JSON body *)
+
let body = Body.json {|{"name": "Alice", "age": 30}|}
+
+
(* Form data *)
+
let body = Body.form [
+
("username", "alice");
+
("password", "secret")
+
]
+
+
(* File upload *)
+
let body = Body.of_file ~mime:Mime.pdf (Eio.Path.(fs / "document.pdf"))
+
+
(* Multipart form with file *)
+
let body = Body.multipart [
+
{ name = "field"; filename = None;
+
content_type = Mime.text_plain;
+
content = `String "value" };
+
{ name = "file"; filename = Some "photo.jpg";
+
content_type = Mime.jpeg;
+
content = `File (Eio.Path.(fs / "photo.jpg")) }
+
]
+
]}
+
*)
type t
-
(** Abstract body type *)
+
(** Abstract body type representing HTTP request body content. *)
+
+
(** {1 Basic Constructors} *)
val empty : t
-
(** Empty body *)
+
(** [empty] creates an empty body (no content). *)
val of_string : Mime.t -> string -> t
-
(** Create body from string with MIME type *)
+
(** [of_string mime content] creates a body from a string with the specified MIME type.
+
Example: [of_string Mime.json {|{"key": "value"}|}] *)
val of_stream : ?length:int64 -> Mime.t -> Eio.Flow.source_ty Eio.Resource.t -> t
-
(** Create body from stream with optional content length *)
+
(** [of_stream ?length mime stream] creates a streaming body. If [length] is provided,
+
it will be used for the Content-Length header, otherwise chunked encoding is used. *)
val of_file : ?mime:Mime.t -> _ Eio.Path.t -> t
-
(** Create body from file capability *)
+
(** [of_file ?mime path] creates a body from a file. The MIME type is inferred from
+
the file extension if not provided. *)
-
(** Convenience constructors *)
+
(** {1 Convenience Constructors} *)
val json : string -> t
-
(** Create JSON body from JSON string *)
+
(** [json str] creates a JSON body with Content-Type: application/json. *)
val text : string -> t
-
(** Create plain text body *)
+
(** [text str] creates a plain text body with Content-Type: text/plain. *)
val form : (string * string) list -> t
-
(** Create URL-encoded form body *)
+
(** [form fields] creates a URL-encoded form body with Content-Type: application/x-www-form-urlencoded.
+
Example: [form [("username", "alice"); ("password", "secret")]] *)
-
(** Multipart support *)
+
(** {1 Multipart Support} *)
type 'a part = {
-
name : string;
-
filename : string option;
-
content_type : Mime.t;
-
content : [`String of string | `Stream of Eio.Flow.source_ty Eio.Resource.t | `File of 'a Eio.Path.t];
+
name : string; (** Form field name *)
+
filename : string option; (** Optional filename for file uploads *)
+
content_type : Mime.t; (** MIME type of this part *)
+
content : [
+
| `String of string (** String content *)
+
| `Stream of Eio.Flow.source_ty Eio.Resource.t (** Streaming content *)
+
| `File of 'a Eio.Path.t (** File content *)
+
];
}
+
(** A single part in a multipart body. *)
val multipart : _ part list -> t
-
(** Create multipart body *)
+
(** [multipart parts] creates a multipart/form-data body from a list of parts.
+
This is commonly used for file uploads and complex form submissions.
+
+
Example:
+
{[
+
let body = Body.multipart [
+
{ name = "username"; filename = None;
+
content_type = Mime.text_plain;
+
content = `String "alice" };
+
{ name = "avatar"; filename = Some "photo.jpg";
+
content_type = Mime.jpeg;
+
content = `File (Eio.Path.(fs / "photo.jpg")) }
+
]
+
]}
+
*)
-
(** Properties *)
+
(** {1 Properties} *)
val content_type : t -> Mime.t option
-
(** Get content type *)
+
(** [content_type body] returns the MIME type of the body, if set. *)
val content_length : t -> int64 option
-
(** Get content length if known *)
+
(** [content_length body] returns the content length in bytes, if known.
+
Returns [None] for streaming bodies without a predetermined length. *)
+
+
(** {1 Private API} *)
-
(** Internal conversion for cohttp-eio integration *)
-
val to_cohttp_body : t -> Cohttp_eio.Body.t option
-
(** Convert body to cohttp-eio body format *)
+
(** Internal functions exposed for use by other modules in the library.
+
These are not part of the public API and may change between versions. *)
+
module Private : sig
+
val to_cohttp_body : sw:Eio.Switch.t -> t -> Cohttp_eio.Body.t option
+
(** [to_cohttp_body ~sw body] converts the body to cohttp-eio format.
+
Uses the switch to manage resources like file handles.
+
This function is used internally by the Client module. *)
+
end
+6 -4
stack/requests/lib/client.ml
···
let cohttp_headers = headers_to_cohttp headers in
let cohttp_body = match body with
-
| Some b -> Body.to_cohttp_body b
+
| Some b -> Body.Private.to_cohttp_body ~sw b
| None -> None
in
···
let elapsed = Unix.gettimeofday () -. start_time in
Log.info (fun m -> m "Request completed in %.3f seconds" elapsed);
-
Response.make
+
Response.Private.make
+
~sw
~status
~headers:final_headers
~body:final_body
···
Eio.Flow.copy body sink;
progress_fn ~received:(Option.value total ~default:0L) ~total;
-
Response.close response
+
(* Response auto-closes with switch *)
+
()
with e ->
-
Response.close response;
+
(* Response auto-closes with switch *)
raise e
+71 -6
stack/requests/lib/client.mli
···
-
(** Global client configuration *)
+
(** 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 *)
+
(** Client configuration with clock and network types.
+
The type parameters track the Eio environment capabilities. *)
+
+
(** {1 Client Creation} *)
val create :
?default_headers:Headers.t ->
···
clock:'a Eio.Time.clock ->
net:'b Eio.Net.t ->
unit -> ('a Eio.Time.clock, 'b Eio.Net.t) t
-
(** Create a client with custom configuration *)
+
(** [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
-
(** Create a client with default configuration *)
+
(** [default ~clock ~net] creates a client with default configuration.
+
Equivalent to [create ~clock ~net ()]. *)
-
(** Internal accessors *)
+
(** {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. *)
-
(** {2 HTTP Request Methods} *)
+
(** {1 HTTP Request Methods} *)
val request :
sw:Eio.Switch.t ->
+69 -14
stack/requests/lib/headers.mli
···
-
(** HTTP headers management with case-insensitive keys *)
+
(** HTTP headers management with case-insensitive keys
+
+
This module provides an efficient implementation of HTTP headers with
+
case-insensitive header names as per RFC 7230. Headers can have multiple
+
values for the same key (e.g., multiple Set-Cookie headers).
+
+
{2 Examples}
+
+
{[
+
let headers =
+
Headers.empty
+
|> Headers.content_type Mime.json
+
|> Headers.bearer "token123"
+
|> Headers.set "X-Custom" "value"
+
]}
+
*)
type t
-
(** Abstract header collection type *)
+
(** Abstract header collection type. Headers are stored with case-insensitive
+
keys and maintain insertion order. *)
+
+
(** {1 Creation and Conversion} *)
val empty : t
-
(** Empty header collection *)
+
(** [empty] creates an empty header collection. *)
val of_list : (string * string) list -> t
-
(** Create headers from association list *)
+
(** [of_list pairs] creates headers from an association list.
+
Later entries override earlier ones for the same key. *)
val to_list : t -> (string * string) list
-
(** Convert to association list *)
+
(** [to_list headers] converts headers to an association list.
+
The order of headers is preserved. *)
+
+
(** {1 Manipulation} *)
val add : string -> string -> t -> t
-
(** Add a header (allows multiple values for same key) *)
+
(** [add name value headers] adds a header value. Multiple values
+
for the same header name are allowed (e.g., for Set-Cookie). *)
val set : string -> string -> t -> t
-
(** Set a header (replaces existing values) *)
+
(** [set name value headers] sets a header value, replacing any
+
existing values for that header name. *)
val get : string -> t -> string option
-
(** Get first value for a header *)
+
(** [get name headers] returns the first value for a header name,
+
or [None] if the header doesn't exist. *)
val get_all : string -> t -> string list
-
(** Get all values for a header *)
+
(** [get_all name headers] returns all values for a header name.
+
Returns an empty list if the header doesn't exist. *)
val remove : string -> t -> t
-
(** Remove all values for a header *)
+
(** [remove name headers] removes all values for a header name. *)
val mem : string -> t -> bool
-
(** Check if header exists *)
+
(** [mem name headers] checks if a header name exists. *)
val merge : t -> t -> t
-
(** Merge two header collections (right overrides left) *)
+
(** [merge base override] merges two header collections.
+
Headers from [override] replace those in [base]. *)
+
+
(** {1 Common Header Builders}
-
(** Common header builders *)
+
Convenience functions for setting common HTTP headers.
+
*)
val content_type : Mime.t -> t -> t
+
(** [content_type mime headers] sets the Content-Type header. *)
+
val content_length : int64 -> t -> t
+
(** [content_length length headers] sets the Content-Length header. *)
+
val accept : Mime.t -> t -> t
+
(** [accept mime headers] sets the Accept header. *)
+
val authorization : string -> t -> t
+
(** [authorization value headers] sets the Authorization header with a raw value. *)
+
val bearer : string -> t -> t
+
(** [bearer token headers] sets the Authorization header with a Bearer token.
+
Example: [bearer "abc123"] sets ["Authorization: Bearer abc123"] *)
+
val basic : username:string -> password:string -> t -> t
+
(** [basic ~username ~password headers] sets the Authorization header with
+
HTTP Basic authentication (base64-encoded username:password). *)
+
val user_agent : string -> t -> t
+
(** [user_agent ua headers] sets the User-Agent header. *)
+
val host : string -> t -> t
+
(** [host hostname headers] sets the Host header. *)
+
val cookie : string -> string -> t -> t
+
(** [cookie name value headers] adds a cookie to the Cookie header.
+
Multiple cookies can be added by calling this function multiple times. *)
+
val range : start:int64 -> ?end_:int64 -> unit -> t -> t
+
(** [range ~start ?end_ () headers] sets the Range header for partial content.
+
Example: [range ~start:0L ~end_:999L ()] requests the first 1000 bytes. *)
+
+
(** {1 Aliases} *)
-
(** Get multiple values for a header (alias for get_all) *)
val get_multi : string -> t -> string list
+
(** [get_multi] is an alias for {!get_all}. *)
(** Pretty printer for headers *)
val pp : Format.formatter -> t -> unit
+178 -13
stack/requests/lib/requests.mli
···
-
(** OCaml HTTP client library with streaming support *)
+
(** Requests - A modern HTTP client library for OCaml
+
+
Requests is an HTTP client library for OCaml inspired by Python's requests
+
and urllib3 libraries. It provides a simple, intuitive API for making HTTP
+
requests while handling complexities like TLS configuration, connection
+
pooling, retries, and cookie management.
+
+
{2 High-Level API}
+
+
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")
+
~body:(Requests.Body.json {|{"name": "Alice"}|})
+
"https://api.example.com/users"
+
+
(* No cleanup needed - responses auto-close with the switch *)
+
]}
+
+
{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 Timeouts}: Configurable connection and read timeouts
+
- {b Cookie Management}: Automatic cookie handling with persistence
+
- {b TLS/SSL}: Secure connections with certificate verification
+
- {b Error Handling}: Comprehensive error types and recovery
+
+
{2 Common Use Cases}
+
+
{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
+
|> Eio.Flow.read_all in
+
print_endline body_text
+
(* Response auto-closes with switch *)
+
]}
+
+
{b File uploads:}
+
{[
+
let body = Requests.Body.multipart [
+
{ name = "file"; filename = Some "document.pdf";
+
content_type = Requests.Mime.pdf;
+
content = `File (Eio.Path.(fs / "document.pdf")) };
+
{ name = "description"; filename = None;
+
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
+
- Custom connection pooling
+
- 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
+
+
(** Cookie storage and management *)
+
module Cookie_jar = Cookie_jar
+
+
(** Retry policies and backoff strategies *)
+
module Retry = Retry
+
+
(** {1 Low-Level Client API}
-
(** {1 Core Types} *)
+
The Client module provides direct control over HTTP requests without
+
session state. Use this for stateless operations or when you need
+
fine-grained control.
+
*)
-
module Status = Status
-
module Method = Method
-
module Mime = Mime
+
(** Low-level HTTP client with connection pooling *)
+
module Client = Client
+
+
(** {1 Core Types}
+
+
These modules define the fundamental types used throughout the library.
+
*)
+
+
(** HTTP response handling *)
+
module Response = Response
+
+
(** Request body construction and encoding *)
+
module Body = Body
+
+
(** HTTP headers manipulation *)
module Headers = Headers
+
+
(** Authentication schemes (Basic, Bearer, OAuth, etc.) *)
module Auth = Auth
-
module Timeout = Timeout
-
module Body = Body
-
module Response = Response
-
module Client = Client
+
+
(** Error types and exception handling *)
module Error = Error
+
(** {1 Supporting Types} *)
+
+
(** HTTP status codes and reason phrases *)
+
module Status = Status
-
(** {1 Session Interface} *)
+
(** HTTP request methods (GET, POST, etc.) *)
+
module Method = Method
+
+
(** MIME types for content negotiation *)
+
module Mime = Mime
-
module Session = Session
-
module Cookie_jar = Cookie_jar
-
module Retry = Retry
+
(** Timeout configuration for requests *)
+
module Timeout = Timeout
+28 -14
stack/requests/lib/response.ml
···
mutable closed : bool;
}
-
let make ~status ~headers ~body ~url ~elapsed =
+
let make ~sw ~status ~headers ~body ~url ~elapsed =
Log.debug (fun m -> m "Creating response: status=%d url=%s elapsed=%.3fs" status url elapsed);
-
{ status; headers; body; url; elapsed; closed = false }
+
let response = { status; headers; body; url; elapsed; closed = false } in
+
+
(* Register cleanup with switch *)
+
Eio.Switch.on_release sw (fun () ->
+
if not response.closed then begin
+
Log.debug (fun m -> m "Auto-closing response for %s via switch" url);
+
try
+
(* Read and discard remaining data *)
+
let rec drain () =
+
let buf = Cstruct.create 8192 in
+
match Eio.Flow.single_read body buf with
+
| 0 -> () (* EOF *)
+
| _ -> drain ()
+
in
+
drain ();
+
response.closed <- true
+
with _ ->
+
response.closed <- true
+
end
+
);
+
+
response
let status t = Status.of_int t.status
···
else
t.body
-
let close t =
-
if not t.closed then begin
-
Log.debug (fun m -> m "Closing response for %s" t.url);
-
(* Consume remaining body if any *)
-
try
-
(* Read and discard remaining data by copying to a buffer *)
-
(* TODO make this a more efficient null sink *)
-
let buf = Buffer.create 4096 in
-
Eio.Flow.copy t.body (Eio.Flow.buffer_sink buf)
-
with _ -> ();
-
t.closed <- true
-
end
(* Pretty printers *)
let pp ppf t =
···
@[%a@]@]"
Status.pp_hum (Status.of_int t.status) t.url t.elapsed
Headers.pp t.headers
+
+
(* Private module *)
+
module Private = struct
+
let make = make
+
end
+83 -30
stack/requests/lib/response.mli
···
-
(** HTTP response handling *)
+
(** HTTP response handling
+
+
This module represents HTTP responses and provides functions to access
+
status codes, headers, and response bodies. Responses support streaming
+
to efficiently handle large payloads.
+
+
{2 Examples}
+
+
{[
+
(* Check response status *)
+
if Response.ok response then
+
Printf.printf "Success!\n"
+
else
+
Printf.printf "Error: %d\n" (Response.status_code response);
+
+
(* Access headers *)
+
match Response.content_type response with
+
| Some mime -> Printf.printf "Type: %s\n" (Mime.to_string mime)
+
| None -> ()
+
+
(* Stream response body *)
+
let body = Response.body response in
+
Eio.Flow.copy body (Eio.Flow.buffer_sink buffer)
+
+
(* Response automatically closes when the switch is released *)
+
]}
+
+
{b Note}: Responses are automatically closed when the switch they were
+
created with is released. Manual cleanup is not necessary.
+
*)
open Eio
type t
-
(** Abstract response type *)
+
(** Abstract response type representing an HTTP response. *)
-
(** Status *)
+
(** {1 Status Information} *)
val status : t -> Status.t
-
(** Get HTTP status as Status.t *)
+
(** [status response] returns the HTTP status as a {!Status.t} value. *)
val status_code : t -> int
-
(** Get HTTP status code as integer *)
+
(** [status_code response] returns the HTTP status code as an integer (e.g., 200, 404). *)
val ok : t -> bool
-
(** Returns true if status is 200-299 (alias for Status.is_success) *)
+
(** [ok response] returns [true] if the status code is in the 2xx success range.
+
This is an alias for {!Status.is_success}. *)
-
(** Headers *)
+
(** {1 Header Access} *)
val headers : t -> Headers.t
-
(** Get all response headers *)
+
(** [headers response] returns all response headers. *)
val header : string -> t -> string option
-
(** Get a specific header value *)
+
(** [header name response] returns the value of a specific header, or [None] if not present.
+
Header names are case-insensitive. *)
val content_type : t -> Mime.t option
-
(** Get content type if present *)
+
(** [content_type response] returns the parsed Content-Type header as a MIME type,
+
or [None] if the header is not present or cannot be parsed. *)
val content_length : t -> int64 option
-
(** Get content length if present *)
+
(** [content_length response] returns the Content-Length in bytes,
+
or [None] if not specified or chunked encoding is used. *)
val location : t -> string option
-
(** Get Location header for redirects *)
+
(** [location response] returns the Location header value, typically used in redirects.
+
Returns [None] if the header is not present. *)
-
(** Metadata *)
+
(** {1 Response Metadata} *)
val url : t -> string
-
(** Final URL after any redirects *)
+
(** [url response] returns the final URL after following any redirects.
+
This may differ from the originally requested URL. *)
val elapsed : t -> float
-
(** Time taken for the request in seconds *)
+
(** [elapsed response] returns the time taken for the request in seconds,
+
including connection establishment, sending the request, and receiving headers. *)
-
(** Body access - streaming *)
+
(** {1 Response Body} *)
val body : t -> Flow.source_ty Resource.t
-
(** Get response body as a flow for streaming *)
+
(** [body response] returns the response body as an Eio flow for streaming.
+
This allows efficient processing of large responses without loading them
+
entirely into memory.
-
val close : t -> unit
-
(** Close the response and free resources *)
+
Example:
+
{[
+
let body = Response.body response in
+
let buffer = Buffer.create 4096 in
+
Eio.Flow.copy body (Eio.Flow.buffer_sink buffer);
+
Buffer.contents buffer
+
]}
+
*)
-
(** Internal construction - not exposed in public API *)
-
val make :
-
status:int ->
-
headers:Headers.t ->
-
body:Flow.source_ty Resource.t ->
-
url:string ->
-
elapsed:float ->
-
t
-
-
(** Pretty printers *)
+
(** {1 Pretty Printing} *)
val pp : Format.formatter -> t -> unit
(** Pretty print a response summary *)
val pp_detailed : Format.formatter -> t -> unit
-
(** Pretty print a response with full headers *)
+
(** Pretty print a response with full headers *)
+
+
(** {1 Private API} *)
+
+
(** Internal functions exposed for use by other modules in the library.
+
These are not part of the public API and may change between versions. *)
+
module Private : sig
+
val make :
+
sw:Eio.Switch.t ->
+
status:int ->
+
headers:Headers.t ->
+
body:Flow.source_ty Resource.t ->
+
url:string ->
+
elapsed:float ->
+
t
+
(** [make ~sw ~status ~headers ~body ~url ~elapsed] constructs a response.
+
The response will be automatically closed when the switch is released.
+
This function is used internally by the Client module. *)
+
end
+1 -24
stack/requests/lib/session.ml
···
(* 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");
Cookie_jar.save ?xdg session.cookie_jar
···
);
session
-
-
let close t =
-
Log.info (fun m -> m "Closing session after %d requests" t.requests_made);
-
if t.persist_cookies && Option.is_some t.xdg then
-
Cookie_jar.save ?xdg:t.xdg t.cookie_jar
-
-
let with_session ~sw ?client ?cookie_jar ?default_headers ?auth ?timeout
-
?follow_redirects ?max_redirects ?verify_tls ?retry ?persist_cookies
-
?xdg env f =
-
let session = create ~sw ?client ?cookie_jar ?default_headers ?auth
-
?timeout ?follow_redirects ?max_redirects ?verify_tls ?retry
-
?persist_cookies ?xdg env in
-
try
-
let result = f session in
-
close session;
-
result
-
with exn ->
-
close session;
-
raise exn
let save_cookies : ('a, 'b) t -> unit = fun t ->
if t.persist_cookies && Option.is_some t.xdg then
···
max_redirects : int;
user_agent : string option;
}
-
-
(* default_config requires Xdge.Cmd.t which can only come from cmdliner parsing.
-
Users should use config_term to get a properly configured session. *)
-
let default_config _app_name _xdg =
-
failwith "Session.Cmd.default_config: Use config_term instead to get configuration from cmdliner"
let create config env sw =
let xdg, _xdg_cmd = config.xdg in
-22
stack/requests/lib/session.mli
···
@param xdg XDG directory configuration (creates default "requests" if not provided)
*)
-
val with_session :
-
sw:Eio.Switch.t ->
-
?client:('clock Eio.Time.clock,'net Eio.Net.t) Client.t ->
-
?cookie_jar:Cookie_jar.t ->
-
?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 -> 'a) ->
-
'a
-
(** Create a session and run a function with it, ensuring cleanup.
-
The session is automatically closed when the function returns. *)
-
(** {1 Configuration Management} *)
val set_default_header : ('clock, 'net) t -> string -> string -> unit
···
max_redirects : int; (** Maximum number of redirects *)
user_agent : string option; (** User-Agent header *)
}
-
-
val default_config : string -> Xdge.t -> config
-
(** [default_config app_name xdg] creates a default configuration *)
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 *)