···11+## v1.0.0 (dev)
22+33+- Initial public vibing.
+91-24
README.md
···6677- **Clean Eio-style API**: Async I/O using OCaml 5's Eio library
88- **Automatic TLS**: Built-in TLS support with automatic CA certificate handling
99-- **Simple Interface**: Intuitive API similar to Python's requests library
1010-- **Type-safe**: Leverages OCaml's type system for safer HTTP operations
1111-- **Comprehensive**: Support for common HTTP methods, headers, authentication, retries, and timeouts
99+- **Connection Pooling**: Efficient connection reuse via session API
1010+- **Authentication**: Basic, Bearer, and Digest authentication (RFC 7617, 6750, 7616)
1111+- **Cookies**: Automatic cookie handling with optional persistence
1212+- **Retries**: Exponential backoff with jitter
1313+- **Timeouts**: Configurable connection and read timeouts
1414+- **Proxy Support**: HTTP/HTTPS proxies with CONNECT tunneling
1515+1616+## Installation
1717+1818+```
1919+opam install requests
2020+```
12211322## Usage
14231515-Basic GET request:
2424+### Session API (Recommended)
2525+2626+Use sessions for connection pooling, cookie persistence, and shared configuration:
16271728```ocaml
1829let () =
3030+ Mirage_crypto_rng_unix.use_default ();
1931 Eio_main.run @@ fun env ->
2020- let response = Requests.get ~env (Uri.of_string "https://example.com") in
2121- print_endline (Requests.Response.body_string response)
3232+ Eio.Switch.run @@ fun sw ->
3333+3434+ (* Create a session with connection pooling *)
3535+ let session = Requests.create ~sw env in
3636+3737+ (* Make a GET request *)
3838+ let response = Requests.get session "https://httpbin.org/get" in
3939+ Printf.printf "Status: %d\n" (Requests.Response.status_code response);
4040+ let body = Requests.Response.text response in
4141+ Printf.printf "Body: %s\n" body
2242```
23432424-POST request with JSON body:
4444+### POST with JSON
25452646```ocaml
2747let () =
4848+ Mirage_crypto_rng_unix.use_default ();
2849 Eio_main.run @@ fun env ->
2929- let uri = Uri.of_string "https://api.example.com/data" in
3030- let body = {|{"key": "value"}|} in
3131- let headers = Requests.Headers.of_list [
3232- ("content-type", "application/json")
3333- ] in
3434- let response = Requests.post ~env ~headers ~body uri in
5050+ Eio.Switch.run @@ fun sw ->
5151+5252+ let session = Requests.create ~sw env in
5353+ let headers = Requests.Headers.(empty |> content_type Requests.Mime.json) in
5454+ let body = Requests.Body.string ~content_type:Requests.Mime.json
5555+ {|{"key": "value"}|} in
5656+ let response = Requests.post session ~headers ~body
5757+ "https://httpbin.org/post" in
3558 Printf.printf "Status: %d\n" (Requests.Response.status_code response)
3659```
37603838-Using authentication and custom headers:
6161+### Authentication
39624063```ocaml
4164let () =
6565+ Mirage_crypto_rng_unix.use_default ();
4266 Eio_main.run @@ fun env ->
6767+ Eio.Switch.run @@ fun sw ->
6868+6969+ let session = Requests.create ~sw env in
7070+7171+ (* Basic authentication *)
4372 let auth = Requests.Auth.basic ~username:"user" ~password:"pass" in
4444- let headers = Requests.Headers.add
4545- (Requests.Headers.init ())
4646- "user-agent" "my-app/1.0" in
4747- let response = Requests.get ~env ~auth ~headers
4848- (Uri.of_string "https://api.example.com") in
4949- (* Process response *)
5050- ()
7373+ let response = Requests.get session ~auth "https://httpbin.org/basic-auth/user/pass" in
7474+ Printf.printf "Status: %d\n" (Requests.Response.status_code response);
7575+7676+ (* Bearer token *)
7777+ let auth = Requests.Auth.bearer ~token:"your-token" in
7878+ let response = Requests.get session ~auth "https://api.example.com/resource" in
7979+ ignore response
5180```
52815353-## Installation
8282+### One-Shot API
8383+8484+For simple, stateless requests without connection pooling:
54858686+```ocaml
8787+let () =
8888+ Mirage_crypto_rng_unix.use_default ();
8989+ Eio_main.run @@ fun env ->
9090+ Eio.Switch.run @@ fun sw ->
9191+9292+ let response = Requests.One.get ~sw
9393+ ~clock:env#clock ~net:env#net
9494+ "https://httpbin.org/get" in
9595+ Printf.printf "Status: %d\n" (Requests.Response.status_code response)
5596```
5656-opam install requests
9797+9898+## Command-Line Tool
9999+100100+The library includes `ocurl`, a curl-like command-line tool:
101101+102102+```bash
103103+# GET request
104104+ocurl https://httpbin.org/get
105105+106106+# POST with data
107107+ocurl -X POST -d '{"key":"value"}' -H "Content-Type: application/json" https://httpbin.org/post
108108+109109+# With authentication
110110+ocurl -u user:pass https://httpbin.org/basic-auth/user/pass
57111```
5811259113## Documentation
601146161-API documentation is available at https://tangled.org/@anil.recoil.org/ocaml-requests or via:
115115+API documentation is available via:
6211663117```
64118opam install requests
65119odig doc requests
66120```
121121+122122+Or build locally:
123123+124124+```
125125+opam exec -- dune build @doc
126126+open _build/default/_doc/_html/index.html
127127+```
128128+129129+## Requirements
130130+131131+- OCaml 5.1.0+
132132+- Eio library
133133+- TLS support via tls-eio and ca-certs
6713468135## License
69136
+19-2
TODO.md
···11-- Auth.Digest seems incomplete
22-- Body MIME guessing seems like it could
11+# Future Work
22+33+## Not Yet Implemented
44+55+- HTTP/2 support (RFC 9113 present in spec/)
66+- Certificate/public key pinning
77+- Request/response middleware system
88+- Progress callbacks for uploads/downloads
99+- Request cancellation
1010+- Unix domain socket support
1111+1212+## Testing
1313+1414+- Expand unit test coverage for individual modules
1515+- Add more edge case tests for HTTP date parsing
1616+1717+## Documentation
1818+1919+- Add troubleshooting guide to README
···264264 let to_string = function
265265 | Empty -> ""
266266 | String { content; _ } -> content
267267- | Stream _ -> failwith "Cannot convert streaming body to string for connection pooling (body must be materialized first)"
268268- | File _ -> failwith "Cannot convert file body to string for connection pooling (file must be read first)"
269269- | Multipart _ -> failwith "Cannot convert multipart body to string for connection pooling (must be encoded first)"
267267+ | Stream _ -> invalid_arg "Body.Private.to_string: cannot convert streaming body (must be materialized first)"
268268+ | File _ -> invalid_arg "Body.Private.to_string: cannot convert file body (must be read first)"
269269+ | Multipart _ -> invalid_arg "Body.Private.to_string: cannot convert multipart body (must be encoded first)"
270270271271 let is_empty = function
272272 | Empty -> true
+3-4
lib/one.ml
···6060 Log.debug (fun m -> m "Resolved %s, connecting..." host);
6161 Eio.Net.connect ~sw net addr
6262 | [] ->
6363- let msg = Printf.sprintf "Failed to resolve hostname: %s" host in
6464- Log.err (fun m -> m "%s" msg);
6565- failwith msg
6363+ Log.err (fun m -> m "Failed to resolve hostname: %s" host);
6464+ raise (Error.err (Error.Dns_resolution_failed { hostname = host }))
66656766(** Minimum TLS version configuration.
6867 Per Recommendation #6: Allow enforcing minimum TLS version. *)
···142141 (* Extract host and port *)
143142 let host = match Uri.host uri with
144143 | Some h -> h
145145- | None -> failwith ("URL must contain a host: " ^ url)
144144+ | None -> raise (Error.err (Error.Invalid_url { url; reason = "URL must contain a host" }))
146145 in
147146148147 let is_https = Uri.scheme uri = Some "https" in
+2-6
lib/requests.ml
···4040 | TLS_1_2 -> `TLS_1_2
4141 | TLS_1_3 -> `TLS_1_3
42424343-(* Note: RNG initialization should be done by the application using
4444- Mirage_crypto_rng_unix.initialize before calling Eio_main.run.
4545- We don't call use_default() here as it spawns background threads
4646- that are incompatible with Eio's structured concurrency. *)
4747-4843(* Main API - Session functionality with connection pooling *)
49445045type t = T : {
···111106 ?(allow_insecure_auth = false)
112107 env =
113108109109+ Mirage_crypto_rng_unix.use_default (); (* avsm: is this bad to do twice? very common footgun to forget to initialise *)
114110 let clock = env#clock in
115111 let net = env#net in
116112···515511 (* Parse the redirect URL to get correct host and port *)
516512 let redirect_host = match Uri.host uri_to_fetch with
517513 | Some h -> h
518518- | None -> failwith "Redirect URL must contain a host"
514514+ | None -> raise (Error.err (Error.Invalid_redirect { url = url_to_fetch; reason = "URL must contain a host" }))
519515 in
520516 let redirect_port = match Uri.scheme uri_to_fetch, Uri.port uri_to_fetch with
521517 | Some "https", None -> 443
+2-2
lib/response.ml
···158158159159let body t =
160160 if t.closed then
161161- failwith "Response has been closed"
161161+ invalid_arg "Response.body: response has been closed"
162162 else
163163 t.body
164164165165let text t =
166166 if t.closed then
167167- failwith "Response has been closed"
167167+ invalid_arg "Response.text: response has been closed"
168168 else
169169 Eio.Buf_read.of_flow t.body ~max_size:max_int |> Eio.Buf_read.take_all
170170
···2020**Status**: Internet Standard (June 2022)
2121**Obsoletes**: RFC 7230
22222323+### RFC 9111 - HTTP Caching
2424+{{:https://datatracker.ietf.org/doc/html/rfc9111}RFC 9111}
2525+2626+Defines the HTTP caching model, including Cache-Control directives, freshness calculations, validation, and cache invalidation.
2727+2828+**Status**: Internet Standard (June 2022)
2929+**Obsoletes**: RFC 7234
3030+2331### RFC 9113 - HTTP/2
2432{{:https://datatracker.ietf.org/doc/html/rfc9113}RFC 9113}
2533···6169Defines how to use bearer tokens in HTTP requests to access OAuth 2.0 protected resources.
62706371**Status**: Proposed Standard (October 2012)
7272+7373+### RFC 7616 - HTTP Digest Access Authentication
7474+{{:https://datatracker.ietf.org/doc/html/rfc7616}RFC 7616}
7575+7676+Specifies the "Digest" HTTP authentication scheme, providing challenge-response authentication with MD5 or SHA-256 hashing.
7777+7878+**Status**: Proposed Standard (September 2015)
7979+**Obsoletes**: RFC 2617
64806581## State Management
6682···8810489105**Status**: Proposed Standard (August 2018)
90106**Obsoletes**: RFC 5077, 5246
107107+108108+## Content Encoding
109109+110110+### RFC 1950 - ZLIB Compressed Data Format
111111+{{:https://datatracker.ietf.org/doc/html/rfc1950}RFC 1950}
112112+113113+Defines the ZLIB compressed data format used for HTTP content encoding with the "deflate" Content-Encoding.
114114+115115+**Status**: Informational (May 1996)
116116+117117+### RFC 1951 - DEFLATE Compressed Data Format
118118+{{:https://datatracker.ietf.org/doc/html/rfc1951}RFC 1951}
119119+120120+Specifies the DEFLATE compression algorithm used in gzip and zlib compression.
121121+122122+**Status**: Informational (May 1996)
9112392124## Using These Specifications
93125
+6-1
test/test_one.ml
···11-(* Test using One module directly without connection pooling *)
11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Test using One module directly without connection pooling *)
27let () =
38 Mirage_crypto_rng_unix.use_default ();
49 Eio_main.run @@ fun env ->
+6-1
test/test_simple_head.ml
···11-(* Simple test to isolate the issue - tests One module directly *)
11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Simple test to isolate the issue - tests One module directly *)
27let () =
38 Logs.set_level (Some Logs.Debug);
49 Logs.set_reporter (Logs_fmt.reporter ());