···216216 else
217217 print_string body_str;
218218219219- (* Close response to free resources *)
220220- Requests.Response.close response;
219219+ (* Response auto-closes with switch *)
221220222221 if not quiet && Requests.Response.ok response then
223222 Logs.app (fun m -> m "✓ Success")
+118-19
stack/requests/lib/body.ml
···8383 (* Complex to calculate, handled during sending *)
8484 None
85858686-let to_cohttp_body = function
8686+(* Strings_source - A flow source that streams from a doubly-linked list of strings/flows *)
8787+module Strings_source = struct
8888+ type element =
8989+ | String of string
9090+ | Flow of Eio.Flow.source_ty Eio.Resource.t
9191+9292+ type t = {
9393+ dllist : element Lwt_dllist.t;
9494+ mutable current_element : element option;
9595+ mutable string_offset : int;
9696+ }
9797+9898+ let rec single_read t dst =
9999+ match t.current_element with
100100+ | None ->
101101+ (* Try to get the first element from the list *)
102102+ if Lwt_dllist.is_empty t.dllist then
103103+ raise End_of_file
104104+ else begin
105105+ t.current_element <- Some (Lwt_dllist.take_l t.dllist);
106106+ single_read t dst
107107+ end
108108+ | Some (String s) when t.string_offset >= String.length s ->
109109+ (* Current string exhausted, move to next element *)
110110+ t.current_element <- None;
111111+ t.string_offset <- 0;
112112+ single_read t dst
113113+ | Some (String s) ->
114114+ (* Read from current string *)
115115+ let available = String.length s - t.string_offset in
116116+ let to_read = min (Cstruct.length dst) available in
117117+ Cstruct.blit_from_string s t.string_offset dst 0 to_read;
118118+ t.string_offset <- t.string_offset + to_read;
119119+ to_read
120120+ | Some (Flow flow) ->
121121+ (* Read from flow *)
122122+ (try
123123+ let n = Eio.Flow.single_read flow dst in
124124+ if n = 0 then begin
125125+ (* Flow exhausted, move to next element *)
126126+ t.current_element <- None;
127127+ single_read t dst
128128+ end else n
129129+ with End_of_file ->
130130+ t.current_element <- None;
131131+ single_read t dst)
132132+133133+ let read_methods = [] (* No special read methods *)
134134+135135+ let create () = {
136136+ dllist = Lwt_dllist.create ();
137137+ current_element = None;
138138+ string_offset = 0;
139139+ }
140140+141141+ let add_string t s =
142142+ ignore (Lwt_dllist.add_r (String s) t.dllist)
143143+144144+ let add_flow t flow =
145145+ ignore (Lwt_dllist.add_r (Flow flow) t.dllist)
146146+end
147147+148148+let strings_source_create () =
149149+ let t = Strings_source.create () in
150150+ let ops = Eio.Flow.Pi.source (module Strings_source) in
151151+ (t, Eio.Resource.T (t, ops))
152152+153153+let to_cohttp_body ~sw = function
87154 | Empty -> None
88155 | String { content; _ } -> Some (Cohttp_eio.Body.of_string content)
89156 | Stream { source; _ } -> Some source
90157 | File { file; _ } ->
9191- (* Read file content *)
9292- let content = Eio.Path.load file in
9393- Some (Cohttp_eio.Body.of_string content)
158158+ (* Open file and stream it directly without loading into memory *)
159159+ let flow = Eio.Path.open_in ~sw file in
160160+ Some (flow :> Eio.Flow.source_ty Eio.Resource.t)
94161 | Multipart { parts; boundary } ->
9595- (* Create multipart body *)
9696- let buffer = Buffer.create 1024 in
162162+ (* Create a single strings_source with dllist for streaming *)
163163+ let source, flow = strings_source_create () in
164164+97165 List.iter (fun part ->
9898- Buffer.add_string buffer (Printf.sprintf "--%s\r\n" boundary);
9999- Buffer.add_string buffer (Printf.sprintf "Content-Disposition: form-data; name=\"%s\"" part.name);
166166+ (* Add boundary *)
167167+ Strings_source.add_string source "--";
168168+ Strings_source.add_string source boundary;
169169+ Strings_source.add_string source "\r\n";
170170+171171+ (* Add Content-Disposition header *)
172172+ Strings_source.add_string source "Content-Disposition: form-data; name=\"";
173173+ Strings_source.add_string source part.name;
174174+ Strings_source.add_string source "\"";
100175 (match part.filename with
101101- | Some f -> Buffer.add_string buffer (Printf.sprintf "; filename=\"%s\"" f)
176176+ | Some f ->
177177+ Strings_source.add_string source "; filename=\"";
178178+ Strings_source.add_string source f;
179179+ Strings_source.add_string source "\""
102180 | None -> ());
103103- Buffer.add_string buffer "\r\n";
104104- Buffer.add_string buffer (Printf.sprintf "Content-Type: %s\r\n\r\n" (Mime.to_string part.content_type));
181181+ Strings_source.add_string source "\r\n";
182182+183183+ (* Add Content-Type header *)
184184+ Strings_source.add_string source "Content-Type: ";
185185+ Strings_source.add_string source (Mime.to_string part.content_type);
186186+ Strings_source.add_string source "\r\n\r\n";
187187+188188+ (* Add content *)
105189 (match part.content with
106106- | `String s -> Buffer.add_string buffer s
190190+ | `String s ->
191191+ Strings_source.add_string source s
107192 | `File file ->
108108- (* Read file content for multipart *)
109109- let content = Eio.Path.load file in
110110- Buffer.add_string buffer content
111111- | `Stream _ -> ()); (* TODO: read stream *)
112112- Buffer.add_string buffer "\r\n"
193193+ (* Open file and add as flow *)
194194+ let file_flow = Eio.Path.open_in ~sw file in
195195+ Strings_source.add_flow source (file_flow :> Eio.Flow.source_ty Eio.Resource.t)
196196+ | `Stream stream ->
197197+ (* Add stream directly *)
198198+ Strings_source.add_flow source stream);
199199+200200+ (* Add trailing newline *)
201201+ Strings_source.add_string source "\r\n"
113202 ) parts;
114114- Buffer.add_string buffer (Printf.sprintf "--%s--\r\n" boundary);
115115- Some (Cohttp_eio.Body.of_string (Buffer.contents buffer))
203203+204204+ (* Add final boundary *)
205205+ Strings_source.add_string source "--";
206206+ Strings_source.add_string source boundary;
207207+ Strings_source.add_string source "--\r\n";
208208+209209+ Some flow
210210+211211+(* Private module *)
212212+module Private = struct
213213+ let to_cohttp_body = to_cohttp_body
214214+end
+88-22
stack/requests/lib/body.mli
···11-(** Request body construction *)
11+(** HTTP request body construction
22+33+ This module provides various ways to construct HTTP request bodies,
44+ including strings, files, streams, forms, and multipart data.
55+66+ {2 Examples}
77+88+ {[
99+ (* Simple text body *)
1010+ let body = Body.text "Hello, World!"
1111+1212+ (* JSON body *)
1313+ let body = Body.json {|{"name": "Alice", "age": 30}|}
1414+1515+ (* Form data *)
1616+ let body = Body.form [
1717+ ("username", "alice");
1818+ ("password", "secret")
1919+ ]
2020+2121+ (* File upload *)
2222+ let body = Body.of_file ~mime:Mime.pdf (Eio.Path.(fs / "document.pdf"))
2323+2424+ (* Multipart form with file *)
2525+ let body = Body.multipart [
2626+ { name = "field"; filename = None;
2727+ content_type = Mime.text_plain;
2828+ content = `String "value" };
2929+ { name = "file"; filename = Some "photo.jpg";
3030+ content_type = Mime.jpeg;
3131+ content = `File (Eio.Path.(fs / "photo.jpg")) }
3232+ ]
3333+ ]}
3434+*)
235336type t
44-(** Abstract body type *)
3737+(** Abstract body type representing HTTP request body content. *)
3838+3939+(** {1 Basic Constructors} *)
540641val empty : t
77-(** Empty body *)
4242+(** [empty] creates an empty body (no content). *)
843944val of_string : Mime.t -> string -> t
1010-(** Create body from string with MIME type *)
4545+(** [of_string mime content] creates a body from a string with the specified MIME type.
4646+ Example: [of_string Mime.json {|{"key": "value"}|}] *)
11471248val of_stream : ?length:int64 -> Mime.t -> Eio.Flow.source_ty Eio.Resource.t -> t
1313-(** Create body from stream with optional content length *)
4949+(** [of_stream ?length mime stream] creates a streaming body. If [length] is provided,
5050+ it will be used for the Content-Length header, otherwise chunked encoding is used. *)
14511552val of_file : ?mime:Mime.t -> _ Eio.Path.t -> t
1616-(** Create body from file capability *)
5353+(** [of_file ?mime path] creates a body from a file. The MIME type is inferred from
5454+ the file extension if not provided. *)
17551818-(** Convenience constructors *)
5656+(** {1 Convenience Constructors} *)
19572058val json : string -> t
2121-(** Create JSON body from JSON string *)
5959+(** [json str] creates a JSON body with Content-Type: application/json. *)
22602361val text : string -> t
2424-(** Create plain text body *)
6262+(** [text str] creates a plain text body with Content-Type: text/plain. *)
25632664val form : (string * string) list -> t
2727-(** Create URL-encoded form body *)
6565+(** [form fields] creates a URL-encoded form body with Content-Type: application/x-www-form-urlencoded.
6666+ Example: [form [("username", "alice"); ("password", "secret")]] *)
28672929-(** Multipart support *)
6868+(** {1 Multipart Support} *)
30693170type 'a part = {
3232- name : string;
3333- filename : string option;
3434- content_type : Mime.t;
3535- content : [`String of string | `Stream of Eio.Flow.source_ty Eio.Resource.t | `File of 'a Eio.Path.t];
7171+ name : string; (** Form field name *)
7272+ filename : string option; (** Optional filename for file uploads *)
7373+ content_type : Mime.t; (** MIME type of this part *)
7474+ content : [
7575+ | `String of string (** String content *)
7676+ | `Stream of Eio.Flow.source_ty Eio.Resource.t (** Streaming content *)
7777+ | `File of 'a Eio.Path.t (** File content *)
7878+ ];
3679}
8080+(** A single part in a multipart body. *)
37813882val multipart : _ part list -> t
3939-(** Create multipart body *)
8383+(** [multipart parts] creates a multipart/form-data body from a list of parts.
8484+ This is commonly used for file uploads and complex form submissions.
8585+8686+ Example:
8787+ {[
8888+ let body = Body.multipart [
8989+ { name = "username"; filename = None;
9090+ content_type = Mime.text_plain;
9191+ content = `String "alice" };
9292+ { name = "avatar"; filename = Some "photo.jpg";
9393+ content_type = Mime.jpeg;
9494+ content = `File (Eio.Path.(fs / "photo.jpg")) }
9595+ ]
9696+ ]}
9797+*)
40984141-(** Properties *)
9999+(** {1 Properties} *)
4210043101val content_type : t -> Mime.t option
4444-(** Get content type *)
102102+(** [content_type body] returns the MIME type of the body, if set. *)
4510346104val content_length : t -> int64 option
4747-(** Get content length if known *)
105105+(** [content_length body] returns the content length in bytes, if known.
106106+ Returns [None] for streaming bodies without a predetermined length. *)
107107+108108+(** {1 Private API} *)
481094949-(** Internal conversion for cohttp-eio integration *)
5050-val to_cohttp_body : t -> Cohttp_eio.Body.t option
5151-(** Convert body to cohttp-eio body format *)
110110+(** Internal functions exposed for use by other modules in the library.
111111+ These are not part of the public API and may change between versions. *)
112112+module Private : sig
113113+ val to_cohttp_body : sw:Eio.Switch.t -> t -> Cohttp_eio.Body.t option
114114+ (** [to_cohttp_body ~sw body] converts the body to cohttp-eio format.
115115+ Uses the switch to manage resources like file handles.
116116+ This function is used internally by the Client module. *)
117117+end
+6-4
stack/requests/lib/client.ml
···129129130130 let cohttp_headers = headers_to_cohttp headers in
131131 let cohttp_body = match body with
132132- | Some b -> Body.to_cohttp_body b
132132+ | Some b -> Body.Private.to_cohttp_body ~sw b
133133 | None -> None
134134 in
135135···226226 let elapsed = Unix.gettimeofday () -. start_time in
227227 Log.info (fun m -> m "Request completed in %.3f seconds" elapsed);
228228229229- Response.make
229229+ Response.Private.make
230230+ ~sw
230231 ~status
231232 ~headers:final_headers
232233 ~body:final_body
···294295 Eio.Flow.copy body sink;
295296 progress_fn ~received:(Option.value total ~default:0L) ~total;
296297297297- Response.close response
298298+ (* Response auto-closes with switch *)
299299+ ()
298300 with e ->
299299- Response.close response;
301301+ (* Response auto-closes with switch *)
300302 raise e
+71-6
stack/requests/lib/client.mli
···11-(** Global client configuration *)
11+(** Low-level HTTP client with streaming support
22+33+ The Client module provides a stateless HTTP client with connection pooling,
44+ TLS support, and streaming capabilities. For stateful requests with automatic
55+ cookie handling and persistent configuration, use the {!Session} module instead.
66+77+ {2 Examples}
88+99+ {[
1010+ open Eio_main
1111+1212+ let () = run @@ fun env ->
1313+ Switch.run @@ fun sw ->
1414+1515+ (* Create a client *)
1616+ let client = Client.create ~clock:env#clock ~net:env#net () in
1717+1818+ (* Simple GET request *)
1919+ let response = Client.get ~sw ~client "https://example.com" in
2020+ Printf.printf "Status: %d\n" (Response.status_code response);
2121+ Response.close response;
2222+2323+ (* POST with JSON body *)
2424+ let response = Client.post ~sw ~client
2525+ ~body:(Body.json {|{"key": "value"}|})
2626+ ~headers:(Headers.empty |> Headers.content_type Mime.json)
2727+ "https://api.example.com/data" in
2828+ Response.close response;
2929+3030+ (* Download file with streaming *)
3131+ Client.download ~sw ~client
3232+ "https://example.com/large-file.zip"
3333+ ~sink:(Eio.Path.(fs / "download.zip" |> sink))
3434+ ]}
3535+*)
236337type ('a,'b) t
44-(** Client configuration *)
3838+(** Client configuration with clock and network types.
3939+ The type parameters track the Eio environment capabilities. *)
4040+4141+(** {1 Client Creation} *)
542643val create :
744 ?default_headers:Headers.t ->
···1350 clock:'a Eio.Time.clock ->
1451 net:'b Eio.Net.t ->
1552 unit -> ('a Eio.Time.clock, 'b Eio.Net.t) t
1616-(** Create a client with custom configuration *)
5353+(** [create ?default_headers ?timeout ?max_retries ?retry_backoff ?verify_tls ?tls_config ~clock ~net ()]
5454+ creates a new HTTP client with the specified configuration.
5555+5656+ @param default_headers Headers to include in every request (default: empty)
5757+ @param timeout Default timeout configuration (default: 30s connect, 60s read)
5858+ @param max_retries Maximum number of retries for failed requests (default: 3)
5959+ @param retry_backoff Exponential backoff factor for retries (default: 2.0)
6060+ @param verify_tls Whether to verify TLS certificates (default: true)
6161+ @param tls_config Custom TLS configuration (default: uses system CA certificates)
6262+ @param clock Eio clock for timeouts and scheduling
6363+ @param net Eio network capability for making connections
6464+*)
17651866val default : clock:'a Eio.Time.clock -> net:'b Eio.Net.t -> ('a Eio.Time.clock, 'b Eio.Net.t) t
1919-(** Create a client with default configuration *)
6767+(** [default ~clock ~net] creates a client with default configuration.
6868+ Equivalent to [create ~clock ~net ()]. *)
20692121-(** Internal accessors *)
7070+(** {1 Configuration Access} *)
7171+2272val clock : ('a,'b) t -> 'a
7373+(** [clock client] returns the clock capability. *)
7474+2375val net : ('a,'b) t -> 'b
7676+(** [net client] returns the network capability. *)
7777+2478val default_headers : ('a,'b) t -> Headers.t
7979+(** [default_headers client] returns the default headers. *)
8080+2581val timeout : ('a,'b) t -> Timeout.t
8282+(** [timeout client] returns the timeout configuration. *)
8383+2684val max_retries : ('a,'b) t -> int
8585+(** [max_retries client] returns the maximum retry count. *)
8686+2787val retry_backoff : ('a,'b) t -> float
8888+(** [retry_backoff client] returns the retry backoff factor. *)
8989+2890val verify_tls : ('a,'b) t -> bool
9191+(** [verify_tls client] returns whether TLS verification is enabled. *)
9292+2993val tls_config : ('a,'b) t -> Tls.Config.client option
9494+(** [tls_config client] returns the TLS configuration if set. *)
30953131-(** {2 HTTP Request Methods} *)
9696+(** {1 HTTP Request Methods} *)
32973398val request :
3499 sw:Eio.Switch.t ->
+69-14
stack/requests/lib/headers.mli
···11-(** HTTP headers management with case-insensitive keys *)
11+(** HTTP headers management with case-insensitive keys
22+33+ This module provides an efficient implementation of HTTP headers with
44+ case-insensitive header names as per RFC 7230. Headers can have multiple
55+ values for the same key (e.g., multiple Set-Cookie headers).
66+77+ {2 Examples}
88+99+ {[
1010+ let headers =
1111+ Headers.empty
1212+ |> Headers.content_type Mime.json
1313+ |> Headers.bearer "token123"
1414+ |> Headers.set "X-Custom" "value"
1515+ ]}
1616+*)
217318type t
44-(** Abstract header collection type *)
1919+(** Abstract header collection type. Headers are stored with case-insensitive
2020+ keys and maintain insertion order. *)
2121+2222+(** {1 Creation and Conversion} *)
523624val empty : t
77-(** Empty header collection *)
2525+(** [empty] creates an empty header collection. *)
826927val of_list : (string * string) list -> t
1010-(** Create headers from association list *)
2828+(** [of_list pairs] creates headers from an association list.
2929+ Later entries override earlier ones for the same key. *)
11301231val to_list : t -> (string * string) list
1313-(** Convert to association list *)
3232+(** [to_list headers] converts headers to an association list.
3333+ The order of headers is preserved. *)
3434+3535+(** {1 Manipulation} *)
14361537val add : string -> string -> t -> t
1616-(** Add a header (allows multiple values for same key) *)
3838+(** [add name value headers] adds a header value. Multiple values
3939+ for the same header name are allowed (e.g., for Set-Cookie). *)
17401841val set : string -> string -> t -> t
1919-(** Set a header (replaces existing values) *)
4242+(** [set name value headers] sets a header value, replacing any
4343+ existing values for that header name. *)
20442145val get : string -> t -> string option
2222-(** Get first value for a header *)
4646+(** [get name headers] returns the first value for a header name,
4747+ or [None] if the header doesn't exist. *)
23482449val get_all : string -> t -> string list
2525-(** Get all values for a header *)
5050+(** [get_all name headers] returns all values for a header name.
5151+ Returns an empty list if the header doesn't exist. *)
26522753val remove : string -> t -> t
2828-(** Remove all values for a header *)
5454+(** [remove name headers] removes all values for a header name. *)
29553056val mem : string -> t -> bool
3131-(** Check if header exists *)
5757+(** [mem name headers] checks if a header name exists. *)
32583359val merge : t -> t -> t
3434-(** Merge two header collections (right overrides left) *)
6060+(** [merge base override] merges two header collections.
6161+ Headers from [override] replace those in [base]. *)
6262+6363+(** {1 Common Header Builders}
35643636-(** Common header builders *)
6565+ Convenience functions for setting common HTTP headers.
6666+*)
37673868val content_type : Mime.t -> t -> t
6969+(** [content_type mime headers] sets the Content-Type header. *)
7070+3971val content_length : int64 -> t -> t
7272+(** [content_length length headers] sets the Content-Length header. *)
7373+4074val accept : Mime.t -> t -> t
7575+(** [accept mime headers] sets the Accept header. *)
7676+4177val authorization : string -> t -> t
7878+(** [authorization value headers] sets the Authorization header with a raw value. *)
7979+4280val bearer : string -> t -> t
8181+(** [bearer token headers] sets the Authorization header with a Bearer token.
8282+ Example: [bearer "abc123"] sets ["Authorization: Bearer abc123"] *)
8383+4384val basic : username:string -> password:string -> t -> t
8585+(** [basic ~username ~password headers] sets the Authorization header with
8686+ HTTP Basic authentication (base64-encoded username:password). *)
8787+4488val user_agent : string -> t -> t
8989+(** [user_agent ua headers] sets the User-Agent header. *)
9090+4591val host : string -> t -> t
9292+(** [host hostname headers] sets the Host header. *)
9393+4694val cookie : string -> string -> t -> t
9595+(** [cookie name value headers] adds a cookie to the Cookie header.
9696+ Multiple cookies can be added by calling this function multiple times. *)
9797+4798val range : start:int64 -> ?end_:int64 -> unit -> t -> t
9999+(** [range ~start ?end_ () headers] sets the Range header for partial content.
100100+ Example: [range ~start:0L ~end_:999L ()] requests the first 1000 bytes. *)
101101+102102+(** {1 Aliases} *)
481034949-(** Get multiple values for a header (alias for get_all) *)
50104val get_multi : string -> t -> string list
105105+(** [get_multi] is an alias for {!get_all}. *)
5110652107(** Pretty printer for headers *)
53108val pp : Format.formatter -> t -> unit
+178-13
stack/requests/lib/requests.mli
···11-(** OCaml HTTP client library with streaming support *)
11+(** Requests - A modern HTTP client library for OCaml
22+33+ Requests is an HTTP client library for OCaml inspired by Python's requests
44+ and urllib3 libraries. It provides a simple, intuitive API for making HTTP
55+ requests while handling complexities like TLS configuration, connection
66+ pooling, retries, and cookie management.
77+88+ {2 High-Level API}
99+1010+ The Requests library offers two main ways to make HTTP requests:
1111+1212+ {b 1. Session-based requests} (Recommended for most use cases)
1313+1414+ Sessions maintain state across requests, handle cookies automatically,
1515+ and provide a simple interface for common tasks:
1616+1717+ {[
1818+ open Eio_main
1919+2020+ let () = run @@ fun env ->
2121+ Switch.run @@ fun sw ->
2222+2323+ (* Create a session *)
2424+ let session = Requests.Session.create ~sw env in
2525+2626+ (* Configure authentication once *)
2727+ Requests.Session.set_auth session (Requests.Auth.bearer "your-token");
2828+2929+ (* Make requests - cookies and auth are handled automatically *)
3030+ let user = Requests.Session.get session "https://api.github.com/user" in
3131+ let repos = Requests.Session.get session "https://api.github.com/user/repos" in
3232+3333+ (* Session automatically manages cookies *)
3434+ let _ = Requests.Session.post session "https://example.com/login"
3535+ ~body:(Requests.Body.form ["username", "alice"; "password", "secret"]) in
3636+ let dashboard = Requests.Session.get session "https://example.com/dashboard"
3737+3838+ (* No cleanup needed - responses auto-close with the switch *)
3939+ ]}
4040+4141+ {b 2. Client-based requests} (For fine-grained control)
4242+4343+ The Client module provides lower-level control when you don't need
4444+ session state or want to manage connections manually:
4545+4646+ {[
4747+ (* Create a client *)
4848+ let client = Requests.Client.create ~clock:env#clock ~net:env#net () in
4949+5050+ (* Make a simple GET request *)
5151+ let response = Requests.Client.get ~sw ~client "https://api.github.com" in
5252+ Printf.printf "Status: %d\n" (Requests.Response.status_code response);
5353+5454+ (* POST with custom headers and body *)
5555+ let response = Requests.Client.post ~sw ~client
5656+ ~headers:(Requests.Headers.empty
5757+ |> Requests.Headers.content_type Requests.Mime.json
5858+ |> Requests.Headers.set "X-API-Key" "secret")
5959+ ~body:(Requests.Body.json {|{"name": "Alice"}|})
6060+ "https://api.example.com/users"
6161+6262+ (* No cleanup needed - responses auto-close with the switch *)
6363+ ]}
6464+6565+ {2 Features}
6666+6767+ - {b Simple API}: Intuitive functions for GET, POST, PUT, DELETE, etc.
6868+ - {b Sessions}: Maintain state (cookies, auth, headers) across requests
6969+ - {b Authentication}: Built-in support for Basic, Bearer, Digest, and OAuth
7070+ - {b Streaming}: Upload and download large files efficiently
7171+ - {b Retries}: Automatic retry with exponential backoff
7272+ - {b Timeouts}: Configurable connection and read timeouts
7373+ - {b Cookie Management}: Automatic cookie handling with persistence
7474+ - {b TLS/SSL}: Secure connections with certificate verification
7575+ - {b Error Handling}: Comprehensive error types and recovery
7676+7777+ {2 Common Use Cases}
7878+7979+ {b Working with JSON APIs:}
8080+ {[
8181+ let response = Requests.Session.post session "https://api.example.com/data"
8282+ ~body:(Requests.Body.json {|{"key": "value"}|}) in
8383+ let body_text =
8484+ Requests.Response.body response
8585+ |> Eio.Flow.read_all in
8686+ print_endline body_text
8787+ (* Response auto-closes with switch *)
8888+ ]}
8989+9090+ {b File uploads:}
9191+ {[
9292+ let body = Requests.Body.multipart [
9393+ { name = "file"; filename = Some "document.pdf";
9494+ content_type = Requests.Mime.pdf;
9595+ content = `File (Eio.Path.(fs / "document.pdf")) };
9696+ { name = "description"; filename = None;
9797+ content_type = Requests.Mime.text_plain;
9898+ content = `String "Important document" }
9999+ ] in
100100+ let response = Requests.Session.post session "https://example.com/upload"
101101+ ~body
102102+ (* Response auto-closes with switch *)
103103+ ]}
104104+105105+ {b Streaming downloads:}
106106+ {[
107107+ Requests.Client.download ~sw ~client
108108+ "https://example.com/large-file.zip"
109109+ ~sink:(Eio.Path.(fs / "download.zip" |> sink))
110110+ ]}
111111+112112+ {2 Choosing Between Session and Client}
113113+114114+ Use {b Session} when you need:
115115+ - Cookie persistence across requests
116116+ - Automatic retry handling
117117+ - Shared authentication across requests
118118+ - Request/response history tracking
119119+ - Configuration persistence to disk
120120+121121+ Use {b Client} when you need:
122122+ - One-off stateless requests
123123+ - Fine-grained control over connections
124124+ - Minimal overhead
125125+ - Custom connection pooling
126126+ - Direct streaming without cookies
127127+*)
128128+129129+(** {1 High-Level Session API}
130130+131131+ Sessions provide stateful HTTP clients with automatic cookie management,
132132+ persistent configuration, and convenient methods for common operations.
133133+*)
134134+135135+(** Stateful HTTP sessions with cookies and configuration persistence *)
136136+module Session = Session
137137+138138+(** Cookie storage and management *)
139139+module Cookie_jar = Cookie_jar
140140+141141+(** Retry policies and backoff strategies *)
142142+module Retry = Retry
143143+144144+(** {1 Low-Level Client API}
214533-(** {1 Core Types} *)
146146+ The Client module provides direct control over HTTP requests without
147147+ session state. Use this for stateless operations or when you need
148148+ fine-grained control.
149149+*)
415055-module Status = Status
66-module Method = Method
77-module Mime = Mime
151151+(** Low-level HTTP client with connection pooling *)
152152+module Client = Client
153153+154154+(** {1 Core Types}
155155+156156+ These modules define the fundamental types used throughout the library.
157157+*)
158158+159159+(** HTTP response handling *)
160160+module Response = Response
161161+162162+(** Request body construction and encoding *)
163163+module Body = Body
164164+165165+(** HTTP headers manipulation *)
8166module Headers = Headers
167167+168168+(** Authentication schemes (Basic, Bearer, OAuth, etc.) *)
9169module Auth = Auth
1010-module Timeout = Timeout
1111-module Body = Body
1212-module Response = Response
1313-module Client = Client
170170+171171+(** Error types and exception handling *)
14172module Error = Error
15173174174+(** {1 Supporting Types} *)
175175+176176+(** HTTP status codes and reason phrases *)
177177+module Status = Status
161781717-(** {1 Session Interface} *)
179179+(** HTTP request methods (GET, POST, etc.) *)
180180+module Method = Method
181181+182182+(** MIME types for content negotiation *)
183183+module Mime = Mime
181841919-module Session = Session
2020-module Cookie_jar = Cookie_jar
2121-module Retry = Retry
185185+(** Timeout configuration for requests *)
186186+module Timeout = Timeout
+28-14
stack/requests/lib/response.ml
···1010 mutable closed : bool;
1111}
12121313-let make ~status ~headers ~body ~url ~elapsed =
1313+let make ~sw ~status ~headers ~body ~url ~elapsed =
1414 Log.debug (fun m -> m "Creating response: status=%d url=%s elapsed=%.3fs" status url elapsed);
1515- { status; headers; body; url; elapsed; closed = false }
1515+ let response = { status; headers; body; url; elapsed; closed = false } in
1616+1717+ (* Register cleanup with switch *)
1818+ Eio.Switch.on_release sw (fun () ->
1919+ if not response.closed then begin
2020+ Log.debug (fun m -> m "Auto-closing response for %s via switch" url);
2121+ try
2222+ (* Read and discard remaining data *)
2323+ let rec drain () =
2424+ let buf = Cstruct.create 8192 in
2525+ match Eio.Flow.single_read body buf with
2626+ | 0 -> () (* EOF *)
2727+ | _ -> drain ()
2828+ in
2929+ drain ();
3030+ response.closed <- true
3131+ with _ ->
3232+ response.closed <- true
3333+ end
3434+ );
3535+3636+ response
16371738let status t = Status.of_int t.status
1839···4869 else
4970 t.body
50715151-let close t =
5252- if not t.closed then begin
5353- Log.debug (fun m -> m "Closing response for %s" t.url);
5454- (* Consume remaining body if any *)
5555- try
5656- (* Read and discard remaining data by copying to a buffer *)
5757- (* TODO make this a more efficient null sink *)
5858- let buf = Buffer.create 4096 in
5959- Eio.Flow.copy t.body (Eio.Flow.buffer_sink buf)
6060- with _ -> ();
6161- t.closed <- true
6262- end
63726473(* Pretty printers *)
6574let pp ppf t =
···7988 @[%a@]@]"
8089 Status.pp_hum (Status.of_int t.status) t.url t.elapsed
8190 Headers.pp t.headers
9191+9292+(* Private module *)
9393+module Private = struct
9494+ let make = make
9595+end
+83-30
stack/requests/lib/response.mli
···11-(** HTTP response handling *)
11+(** HTTP response handling
22+33+ This module represents HTTP responses and provides functions to access
44+ status codes, headers, and response bodies. Responses support streaming
55+ to efficiently handle large payloads.
66+77+ {2 Examples}
88+99+ {[
1010+ (* Check response status *)
1111+ if Response.ok response then
1212+ Printf.printf "Success!\n"
1313+ else
1414+ Printf.printf "Error: %d\n" (Response.status_code response);
1515+1616+ (* Access headers *)
1717+ match Response.content_type response with
1818+ | Some mime -> Printf.printf "Type: %s\n" (Mime.to_string mime)
1919+ | None -> ()
2020+2121+ (* Stream response body *)
2222+ let body = Response.body response in
2323+ Eio.Flow.copy body (Eio.Flow.buffer_sink buffer)
2424+2525+ (* Response automatically closes when the switch is released *)
2626+ ]}
2727+2828+ {b Note}: Responses are automatically closed when the switch they were
2929+ created with is released. Manual cleanup is not necessary.
3030+*)
231332open Eio
433534type t
66-(** Abstract response type *)
3535+(** Abstract response type representing an HTTP response. *)
73688-(** Status *)
3737+(** {1 Status Information} *)
9381039val status : t -> Status.t
1111-(** Get HTTP status as Status.t *)
4040+(** [status response] returns the HTTP status as a {!Status.t} value. *)
12411342val status_code : t -> int
1414-(** Get HTTP status code as integer *)
4343+(** [status_code response] returns the HTTP status code as an integer (e.g., 200, 404). *)
15441645val ok : t -> bool
1717-(** Returns true if status is 200-299 (alias for Status.is_success) *)
4646+(** [ok response] returns [true] if the status code is in the 2xx success range.
4747+ This is an alias for {!Status.is_success}. *)
18481919-(** Headers *)
4949+(** {1 Header Access} *)
20502151val headers : t -> Headers.t
2222-(** Get all response headers *)
5252+(** [headers response] returns all response headers. *)
23532454val header : string -> t -> string option
2525-(** Get a specific header value *)
5555+(** [header name response] returns the value of a specific header, or [None] if not present.
5656+ Header names are case-insensitive. *)
26572758val content_type : t -> Mime.t option
2828-(** Get content type if present *)
5959+(** [content_type response] returns the parsed Content-Type header as a MIME type,
6060+ or [None] if the header is not present or cannot be parsed. *)
29613062val content_length : t -> int64 option
3131-(** Get content length if present *)
6363+(** [content_length response] returns the Content-Length in bytes,
6464+ or [None] if not specified or chunked encoding is used. *)
32653366val location : t -> string option
3434-(** Get Location header for redirects *)
6767+(** [location response] returns the Location header value, typically used in redirects.
6868+ Returns [None] if the header is not present. *)
35693636-(** Metadata *)
7070+(** {1 Response Metadata} *)
37713872val url : t -> string
3939-(** Final URL after any redirects *)
7373+(** [url response] returns the final URL after following any redirects.
7474+ This may differ from the originally requested URL. *)
40754176val elapsed : t -> float
4242-(** Time taken for the request in seconds *)
7777+(** [elapsed response] returns the time taken for the request in seconds,
7878+ including connection establishment, sending the request, and receiving headers. *)
43794444-(** Body access - streaming *)
8080+(** {1 Response Body} *)
45814682val body : t -> Flow.source_ty Resource.t
4747-(** Get response body as a flow for streaming *)
8383+(** [body response] returns the response body as an Eio flow for streaming.
8484+ This allows efficient processing of large responses without loading them
8585+ entirely into memory.
48864949-val close : t -> unit
5050-(** Close the response and free resources *)
8787+ Example:
8888+ {[
8989+ let body = Response.body response in
9090+ let buffer = Buffer.create 4096 in
9191+ Eio.Flow.copy body (Eio.Flow.buffer_sink buffer);
9292+ Buffer.contents buffer
9393+ ]}
9494+*)
51955252-(** Internal construction - not exposed in public API *)
53965454-val make :
5555- status:int ->
5656- headers:Headers.t ->
5757- body:Flow.source_ty Resource.t ->
5858- url:string ->
5959- elapsed:float ->
6060- t
6161-6262-(** Pretty printers *)
9797+(** {1 Pretty Printing} *)
63986499val pp : Format.formatter -> t -> unit
65100(** Pretty print a response summary *)
6610167102val pp_detailed : Format.formatter -> t -> unit
6868-(** Pretty print a response with full headers *)103103+(** Pretty print a response with full headers *)
104104+105105+(** {1 Private API} *)
106106+107107+(** Internal functions exposed for use by other modules in the library.
108108+ These are not part of the public API and may change between versions. *)
109109+module Private : sig
110110+ val make :
111111+ sw:Eio.Switch.t ->
112112+ status:int ->
113113+ headers:Headers.t ->
114114+ body:Flow.source_ty Resource.t ->
115115+ url:string ->
116116+ elapsed:float ->
117117+ t
118118+ (** [make ~sw ~status ~headers ~body ~url ~elapsed] constructs a response.
119119+ The response will be automatically closed when the switch is released.
120120+ This function is used internally by the Client module. *)
121121+end
+1-24
stack/requests/lib/session.ml
···117117118118 (* Register cleanup on switch *)
119119 Eio.Switch.on_release sw (fun () ->
120120+ Log.info (fun m -> m "Closing session after %d requests" session.requests_made);
120121 if persist_cookies && Option.is_some xdg then begin
121122 Log.info (fun m -> m "Saving cookies on session close");
122123 Cookie_jar.save ?xdg session.cookie_jar
···124125 );
125126126127 session
127127-128128-let close t =
129129- Log.info (fun m -> m "Closing session after %d requests" t.requests_made);
130130- if t.persist_cookies && Option.is_some t.xdg then
131131- Cookie_jar.save ?xdg:t.xdg t.cookie_jar
132132-133133-let with_session ~sw ?client ?cookie_jar ?default_headers ?auth ?timeout
134134- ?follow_redirects ?max_redirects ?verify_tls ?retry ?persist_cookies
135135- ?xdg env f =
136136- let session = create ~sw ?client ?cookie_jar ?default_headers ?auth
137137- ?timeout ?follow_redirects ?max_redirects ?verify_tls ?retry
138138- ?persist_cookies ?xdg env in
139139- try
140140- let result = f session in
141141- close session;
142142- result
143143- with exn ->
144144- close session;
145145- raise exn
146128147129let save_cookies : ('a, 'b) t -> unit = fun t ->
148130 if t.persist_cookies && Option.is_some t.xdg then
···361343 max_redirects : int;
362344 user_agent : string option;
363345 }
364364-365365- (* default_config requires Xdge.Cmd.t which can only come from cmdliner parsing.
366366- Users should use config_term to get a properly configured session. *)
367367- let default_config _app_name _xdg =
368368- failwith "Session.Cmd.default_config: Use config_term instead to get configuration from cmdliner"
369346370347 let create config env sw =
371348 let xdg, _xdg_cmd = config.xdg in
-22
stack/requests/lib/session.mli
···8888 @param xdg XDG directory configuration (creates default "requests" if not provided)
8989*)
90909191-val with_session :
9292- sw:Eio.Switch.t ->
9393- ?client:('clock Eio.Time.clock,'net Eio.Net.t) Client.t ->
9494- ?cookie_jar:Cookie_jar.t ->
9595- ?default_headers:Headers.t ->
9696- ?auth:Auth.t ->
9797- ?timeout:Timeout.t ->
9898- ?follow_redirects:bool ->
9999- ?max_redirects:int ->
100100- ?verify_tls:bool ->
101101- ?retry:Retry.config ->
102102- ?persist_cookies:bool ->
103103- ?xdg:Xdge.t ->
104104- < clock: 'clock Eio.Resource.t; net: 'net Eio.Resource.t; fs: Eio.Fs.dir_ty Eio.Path.t; .. > ->
105105- (('clock Eio.Resource.t, 'net Eio.Resource.t) t -> 'a) ->
106106- 'a
107107-(** Create a session and run a function with it, ensuring cleanup.
108108- The session is automatically closed when the function returns. *)
109109-11091(** {1 Configuration Management} *)
1119211293val set_default_header : ('clock, 'net) t -> string -> string -> unit
···344325 max_redirects : int; (** Maximum number of redirects *)
345326 user_agent : string option; (** User-Agent header *)
346327 }
347347-348348- val default_config : string -> Xdge.t -> config
349349- (** [default_config app_name xdg] creates a default configuration *)
350328351329 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
352330 (** [create config env sw] creates a session from command-line configuration *)