···88let src = Logs.Src.create "requests.http_client" ~doc:"Low-level HTTP client"
99module Log = (val Logs.src_log src : Logs.LOG)
10101111+(** Decompression support using the decompress library *)
1212+1313+(** Decompress gzip-encoded data *)
1414+let decompress_gzip data =
1515+ Log.debug (fun m -> m "Decompressing gzip data (%d bytes)" (String.length data));
1616+ let i = De.bigstring_create De.io_buffer_size in
1717+ let o = De.bigstring_create De.io_buffer_size in
1818+ let r = Buffer.create (String.length data * 2) in
1919+ let p = ref 0 in
2020+ let refill buf =
2121+ let len = min (String.length data - !p) De.io_buffer_size in
2222+ Bigstringaf.blit_from_string data ~src_off:!p buf ~dst_off:0 ~len;
2323+ p := !p + len;
2424+ len
2525+ in
2626+ let flush buf len =
2727+ let str = Bigstringaf.substring buf ~off:0 ~len in
2828+ Buffer.add_string r str
2929+ in
3030+ match Gz.Higher.uncompress ~refill ~flush i o with
3131+ | Ok _ ->
3232+ let result = Buffer.contents r in
3333+ Log.debug (fun m -> m "Gzip decompression succeeded: %d -> %d bytes"
3434+ (String.length data) (String.length result));
3535+ Ok result
3636+ | Error (`Msg e) ->
3737+ Log.warn (fun m -> m "Gzip decompression failed: %s" e);
3838+ Error e
3939+4040+(** Decompress deflate-encoded data (raw DEFLATE, RFC 1951) *)
4141+let decompress_deflate data =
4242+ Log.debug (fun m -> m "Decompressing deflate data (%d bytes)" (String.length data));
4343+ let i = De.bigstring_create De.io_buffer_size in
4444+ let o = De.bigstring_create De.io_buffer_size in
4545+ let w = De.make_window ~bits:15 in
4646+ let r = Buffer.create (String.length data * 2) in
4747+ let p = ref 0 in
4848+ let refill buf =
4949+ let len = min (String.length data - !p) De.io_buffer_size in
5050+ Bigstringaf.blit_from_string data ~src_off:!p buf ~dst_off:0 ~len;
5151+ p := !p + len;
5252+ len
5353+ in
5454+ let flush buf len =
5555+ let str = Bigstringaf.substring buf ~off:0 ~len in
5656+ Buffer.add_string r str
5757+ in
5858+ match De.Higher.uncompress ~w ~refill ~flush i o with
5959+ | Ok () ->
6060+ let result = Buffer.contents r in
6161+ Log.debug (fun m -> m "Deflate decompression succeeded: %d -> %d bytes"
6262+ (String.length data) (String.length result));
6363+ Ok result
6464+ | Error (`Msg e) ->
6565+ Log.warn (fun m -> m "Deflate decompression failed: %s" e);
6666+ Error e
6767+6868+(** Decompress zlib-encoded data (DEFLATE with zlib header, RFC 1950) *)
6969+let decompress_zlib data =
7070+ Log.debug (fun m -> m "Decompressing zlib data (%d bytes)" (String.length data));
7171+ let i = De.bigstring_create De.io_buffer_size in
7272+ let o = De.bigstring_create De.io_buffer_size in
7373+ let allocate bits = De.make_window ~bits in
7474+ let r = Buffer.create (String.length data * 2) in
7575+ let p = ref 0 in
7676+ let refill buf =
7777+ let len = min (String.length data - !p) De.io_buffer_size in
7878+ Bigstringaf.blit_from_string data ~src_off:!p buf ~dst_off:0 ~len;
7979+ p := !p + len;
8080+ len
8181+ in
8282+ let flush buf len =
8383+ let str = Bigstringaf.substring buf ~off:0 ~len in
8484+ Buffer.add_string r str
8585+ in
8686+ match Zl.Higher.uncompress ~allocate ~refill ~flush i o with
8787+ | Ok _ ->
8888+ let result = Buffer.contents r in
8989+ Log.debug (fun m -> m "Zlib decompression succeeded: %d -> %d bytes"
9090+ (String.length data) (String.length result));
9191+ Ok result
9292+ | Error (`Msg e) ->
9393+ Log.warn (fun m -> m "Zlib decompression failed: %s" e);
9494+ Error e
9595+9696+(** Decompress body based on Content-Encoding header *)
9797+let decompress_body ~content_encoding body =
9898+ let encoding = String.lowercase_ascii (String.trim content_encoding) in
9999+ match encoding with
100100+ | "gzip" | "x-gzip" ->
101101+ (match decompress_gzip body with
102102+ | Ok decompressed -> decompressed
103103+ | Error _ -> body) (* Fall back to raw body on error *)
104104+ | "deflate" ->
105105+ (* "deflate" in HTTP can mean either raw DEFLATE or zlib-wrapped.
106106+ Many servers send zlib-wrapped data despite the spec. Try zlib first,
107107+ then fall back to raw deflate. *)
108108+ (match decompress_zlib body with
109109+ | Ok decompressed -> decompressed
110110+ | Error _ ->
111111+ match decompress_deflate body with
112112+ | Ok decompressed -> decompressed
113113+ | Error _ -> body)
114114+ | "identity" | "" -> body
115115+ | other ->
116116+ Log.warn (fun m -> m "Unknown Content-Encoding '%s', returning raw body" other);
117117+ body
118118+11119(** Build HTTP/1.1 request as a string *)
12120let build_request ~method_ ~uri ~headers ~body_str =
13121 let path = Uri.path uri in
···167275 in
168276169277 (status, resp_headers, body_str)
278278+279279+(** Make HTTP request with optional auto-decompression *)
280280+let make_request_decompress ~method_ ~uri ~headers ~body_str ~auto_decompress flow =
281281+ let (status, resp_headers, body_str) = make_request ~method_ ~uri ~headers ~body_str flow in
282282+ if auto_decompress then
283283+ let body_str = match Headers.get "content-encoding" resp_headers with
284284+ | Some encoding -> decompress_body ~content_encoding:encoding body_str
285285+ | None -> body_str
286286+ in
287287+ (* Remove Content-Encoding header after decompression since body is now uncompressed *)
288288+ let resp_headers = match Headers.get "content-encoding" resp_headers with
289289+ | Some _ -> Headers.remove "content-encoding" resp_headers
290290+ | None -> resp_headers
291291+ in
292292+ (status, resp_headers, body_str)
293293+ else
294294+ (status, resp_headers, body_str)
+55-8
lib/one.ml
···66let src = Logs.Src.create "requests.one" ~doc:"One-shot HTTP Requests"
77module Log = (val Logs.src_log src : Logs.LOG)
8899+(* Helper to check if two URIs have the same origin for security purposes.
1010+ Used to determine if sensitive headers (Authorization, Cookie) should be
1111+ stripped during redirects. Following Python requests behavior:
1212+ - Same host and same scheme = same origin
1313+ - http -> https upgrade on same host = allowed (more secure)
1414+ TODO: Support .netrc for re-acquiring auth credentials on new hosts *)
1515+let same_origin uri1 uri2 =
1616+ let host1 = Uri.host uri1 |> Option.map String.lowercase_ascii in
1717+ let host2 = Uri.host uri2 |> Option.map String.lowercase_ascii in
1818+ let scheme1 = Uri.scheme uri1 |> Option.value ~default:"http" in
1919+ let scheme2 = Uri.scheme uri2 |> Option.value ~default:"http" in
2020+ match host1, host2 with
2121+ | Some h1, Some h2 when String.equal h1 h2 ->
2222+ (* Same host - allow same scheme or http->https upgrade *)
2323+ String.equal scheme1 scheme2 ||
2424+ (scheme1 = "http" && scheme2 = "https")
2525+ | _ -> false
2626+2727+(* Strip sensitive headers for cross-origin redirects to prevent credential leakage *)
2828+let strip_sensitive_headers headers =
2929+ headers
3030+ |> Headers.remove "Authorization"
3131+932(* Helper to create TCP connection to host:port *)
1033let connect_tcp ~sw ~net ~host ~port =
1134 Log.debug (fun m -> m "Connecting to %s:%d" host port);
···92115(* Main request implementation - completely stateless *)
93116let request ~sw ~clock ~net ?headers ?body ?auth ?timeout
94117 ?(follow_redirects = true) ?(max_redirects = 10)
9595- ?(verify_tls = true) ?tls_config ~method_ url =
118118+ ?(verify_tls = true) ?tls_config ?(auto_decompress = true) ~method_ url =
9611997120 let start_time = Unix.gettimeofday () in
98121 let method_str = Method.to_string method_ in
···112135 |> Option.fold ~none:headers ~some:(fun mime -> Headers.content_type mime headers)
113136 in
114137138138+ (* Add Accept-Encoding header for auto-decompression if not already set *)
139139+ let headers =
140140+ if auto_decompress && not (Headers.mem "Accept-Encoding" headers) then
141141+ Headers.set "Accept-Encoding" "gzip, deflate" headers
142142+ else
143143+ headers
144144+ in
145145+115146 (* Convert body to string for sending *)
116147 let request_body_str = Option.fold ~none:"" ~some:Body.Private.to_string body in
117148118118- (* Execute request with redirects *)
119119- let rec make_with_redirects url_to_fetch redirects_left =
149149+ (* Track the original URL for cross-origin redirect detection *)
150150+ let original_uri = Uri.of_string url in
151151+152152+ (* Execute request with redirects
153153+ headers_for_request: the headers to use for this specific request (may have auth stripped) *)
154154+ let rec make_with_redirects ~headers_for_request url_to_fetch redirects_left =
120155 let uri_to_fetch = Uri.of_string url_to_fetch in
121156122157 (* Connect to URL (opens new TCP connection) *)
123158 let flow = connect_to_url ~sw ~clock ~net ~url:url_to_fetch
124159 ~timeout ~verify_tls ~tls_config in
125160126126- (* Make HTTP request using low-level client *)
161161+ (* Make HTTP request using low-level client with optional auto-decompression *)
127162 let status, resp_headers, response_body_str =
128128- Http_client.make_request ~method_:method_str ~uri:uri_to_fetch
129129- ~headers ~body_str:request_body_str flow
163163+ Http_client.make_request_decompress ~method_:method_str ~uri:uri_to_fetch
164164+ ~headers:headers_for_request ~body_str:request_body_str
165165+ ~auto_decompress flow
130166 in
131167132168 Log.info (fun m -> m "Received response: status=%d" status);
···144180 (status, resp_headers, response_body_str, url_to_fetch)
145181 | Some location ->
146182 Log.info (fun m -> m "Following redirect to %s (%d remaining)" location redirects_left);
147147- make_with_redirects location (redirects_left - 1)
183183+ (* Strip sensitive headers on cross-origin redirects (security)
184184+ Following Python requests behavior: auth headers should not leak to other hosts *)
185185+ let redirect_uri = Uri.of_string location in
186186+ let headers_for_redirect =
187187+ if same_origin original_uri redirect_uri then
188188+ headers_for_request
189189+ else begin
190190+ Log.debug (fun m -> m "Cross-origin redirect detected: stripping Authorization header");
191191+ strip_sensitive_headers headers_for_request
192192+ end
193193+ in
194194+ make_with_redirects ~headers_for_request:headers_for_redirect location (redirects_left - 1)
148195 end else
149196 (status, resp_headers, response_body_str, url_to_fetch)
150197 in
151198152199 let final_status, final_headers, final_body_str, final_url =
153153- make_with_redirects url max_redirects
200200+ make_with_redirects ~headers_for_request:headers url max_redirects
154201 in
155202156203 let elapsed = Unix.gettimeofday () -. start_time in
+4-2
lib/one.mli
···6363 ?max_redirects:int ->
6464 ?verify_tls:bool ->
6565 ?tls_config:Tls.Config.client ->
6666+ ?auto_decompress:bool ->
6667 method_:Method.t ->
6768 string ->
6869 Response.t
6970(** [request ~sw ~clock ~net ?headers ?body ?auth ?timeout ?follow_redirects
7070- ?max_redirects ?verify_tls ?tls_config ~method_ url] makes a single HTTP
7171- request without connection pooling.
7171+ ?max_redirects ?verify_tls ?tls_config ?auto_decompress ~method_ url] makes
7272+ a single HTTP request without connection pooling.
72737374 Each call opens a new TCP connection (with TLS if https://), makes the
7475 request, and closes the connection when the switch closes.
···8485 @param max_redirects Maximum redirects to follow (default: 10)
8586 @param verify_tls Whether to verify TLS certificates (default: true)
8687 @param tls_config Custom TLS configuration (default: system CA certs)
8888+ @param auto_decompress Whether to automatically decompress gzip/deflate responses (default: true)
8789 @param method_ HTTP method (GET, POST, etc.)
8890 @param url URL to request
8991*)
+130-11
lib/requests.ml
···4646 retry : Retry.config option;
4747 persist_cookies : bool;
4848 xdg : Xdge.t option;
4949+ auto_decompress : bool;
49505051 (* Statistics - mutable but NOTE: when sessions are derived via record update
5152 syntax ({t with field = value}), these are copied not shared. Each derived
···7475 ?retry
7576 ?(persist_cookies = false)
7677 ?xdg
7878+ ?(auto_decompress = true)
7779 env =
78807981 let clock = env#clock in
···156158 retry;
157159 persist_cookies;
158160 xdg;
161161+ auto_decompress;
159162 requests_made = 0;
160163 total_time = 0.0;
161164 retries_count = 0;
···186189let cookies (T t) = t.cookie_jar
187190let clear_cookies (T t) = Cookeio_jar.clear t.cookie_jar
188191192192+(* Helper to check if two URIs have the same origin for security purposes.
193193+ Used to determine if sensitive headers (Authorization, Cookie) should be
194194+ stripped during redirects. Following Python requests behavior:
195195+ - Same host and same scheme = same origin
196196+ - http -> https upgrade on same host = allowed (more secure)
197197+ TODO: Support .netrc for re-acquiring auth credentials on new hosts *)
198198+let same_origin uri1 uri2 =
199199+ let host1 = Uri.host uri1 |> Option.map String.lowercase_ascii in
200200+ let host2 = Uri.host uri2 |> Option.map String.lowercase_ascii in
201201+ let scheme1 = Uri.scheme uri1 |> Option.value ~default:"http" in
202202+ let scheme2 = Uri.scheme uri2 |> Option.value ~default:"http" in
203203+ match host1, host2 with
204204+ | Some h1, Some h2 when String.equal h1 h2 ->
205205+ (* Same host - allow same scheme or http->https upgrade *)
206206+ String.equal scheme1 scheme2 ||
207207+ (scheme1 = "http" && scheme2 = "https")
208208+ | _ -> false
209209+210210+(* Strip sensitive headers for cross-origin redirects to prevent credential leakage *)
211211+let strip_sensitive_headers headers =
212212+ headers
213213+ |> Headers.remove "Authorization"
214214+189215(* Internal request function using connection pools *)
190216let make_request_internal (T t) ?headers ?body ?auth ?timeout ?follow_redirects ?max_redirects ~method_ url =
191217 let start_time = Unix.gettimeofday () in
···221247 | None -> headers
222248 in
223249250250+ (* Add Accept-Encoding header for auto-decompression if not already set *)
251251+ let base_headers =
252252+ if t.auto_decompress && not (Headers.mem "Accept-Encoding" base_headers) then
253253+ Headers.set "Accept-Encoding" "gzip, deflate" base_headers
254254+ else
255255+ base_headers
256256+ in
257257+224258 (* Convert body to string for sending *)
225259 let request_body_str = match body with
226260 | None -> ""
···249283 )
250284 in
251285286286+ (* Track the original URL for cross-origin redirect detection *)
287287+ let original_uri = Uri.of_string url in
288288+252289 let response =
253290254254- (* Execute request with redirect handling *)
255255- let rec make_with_redirects url_to_fetch redirects_left =
291291+ (* Execute request with redirect handling
292292+ headers_for_request: the headers to use for this specific request (may have auth stripped) *)
293293+ let rec make_with_redirects ~headers_for_request url_to_fetch redirects_left =
256294 let uri_to_fetch = Uri.of_string url_to_fetch in
257295258296 (* Parse the redirect URL to get correct host and port *)
···292330 match cookies with
293331 | [] ->
294332 Log.debug (fun m -> m "No cookies found for %s%s" fetch_domain fetch_path);
295295- base_headers
333333+ headers_for_request
296334 | cookies ->
297335 let cookie_header = Cookeio.make_cookie_header cookies in
298336 Log.debug (fun m -> m "Adding %d cookies for %s%s: Cookie: %s"
299337 (List.length cookies) fetch_domain fetch_path cookie_header);
300300- Headers.set "Cookie" cookie_header base_headers
338338+ Headers.set "Cookie" cookie_header headers_for_request
301339 )
302340 in
303341···314352 let make_request_fn () =
315353 Conpool.with_connection redirect_pool redirect_endpoint (fun flow ->
316354 (* Flow is already TLS-wrapped if from https_pool, plain TCP if from http_pool *)
317317- (* Use our low-level HTTP client *)
318318- Http_client.make_request ~method_:method_str ~uri:uri_to_fetch
319319- ~headers:headers_with_cookies ~body_str:request_body_str flow
355355+ (* Use our low-level HTTP client with optional auto-decompression *)
356356+ Http_client.make_request_decompress ~method_:method_str ~uri:uri_to_fetch
357357+ ~headers:headers_with_cookies ~body_str:request_body_str
358358+ ~auto_decompress:t.auto_decompress flow
320359 )
321360 in
322361···369408 Uri.to_string resolved
370409 in
371410 Log.info (fun m -> m "Following redirect to %s (%d remaining)" absolute_location redirects_left);
372372- make_with_redirects absolute_location (redirects_left - 1)
411411+ (* Strip sensitive headers on cross-origin redirects (security)
412412+ Following Python requests behavior: auth headers should not leak to other hosts *)
413413+ let redirect_uri = Uri.of_string absolute_location in
414414+ let headers_for_redirect =
415415+ if same_origin original_uri redirect_uri then
416416+ headers_for_request
417417+ else begin
418418+ Log.debug (fun m -> m "Cross-origin redirect detected: stripping Authorization header");
419419+ strip_sensitive_headers headers_for_request
420420+ end
421421+ in
422422+ make_with_redirects ~headers_for_request:headers_for_redirect absolute_location (redirects_left - 1)
373423 end else
374424 (status, resp_headers, response_body_str, url_to_fetch)
375425 in
376426377427 let max_redir = Option.value max_redirects ~default:t.max_redirects in
378428 let final_status, final_headers, final_body_str, final_url =
379379- make_with_redirects url max_redir
429429+ make_with_redirects ~headers_for_request:base_headers url max_redir
380430 in
381431382432 let elapsed = Unix.gettimeofday () -. start_time in
···415465416466 response
417467468468+(* Helper to handle Digest authentication 401 challenge *)
469469+let handle_digest_auth (T t as wrapped_t) ~headers ~body ~auth ~timeout ~follow_redirects ~max_redirects ~method_ ~url response =
470470+ (* Check if we got a 401 and have Digest auth configured *)
471471+ let auth_to_use = match auth with Some a -> a | None -> Option.value t.auth ~default:Auth.none in
472472+ if Response.status_code response = 401 && Auth.is_digest auth_to_use then begin
473473+ match Auth.get_digest_credentials auth_to_use with
474474+ | Some (username, password) ->
475475+ (match Response.header "www-authenticate" response with
476476+ | Some www_auth ->
477477+ (match Auth.parse_www_authenticate www_auth with
478478+ | Some challenge ->
479479+ Log.info (fun m -> m "Received Digest challenge, retrying with authentication");
480480+ let uri = Uri.of_string url in
481481+ let uri_path = Uri.path uri in
482482+ let uri_path = if uri_path = "" then "/" else uri_path in
483483+ (* Apply digest auth to headers *)
484484+ let base_headers = Option.value headers ~default:Headers.empty in
485485+ let auth_headers = Auth.apply_digest
486486+ ~username ~password
487487+ ~method_:(Method.to_string method_)
488488+ ~uri:uri_path
489489+ ~challenge
490490+ base_headers
491491+ in
492492+ (* Retry with Digest auth - use Auth.none to prevent double-application *)
493493+ make_request_internal wrapped_t ~headers:auth_headers ?body ~auth:Auth.none ?timeout
494494+ ?follow_redirects ?max_redirects ~method_ url
495495+ | None ->
496496+ Log.warn (fun m -> m "Could not parse Digest challenge from WWW-Authenticate");
497497+ response)
498498+ | None ->
499499+ Log.warn (fun m -> m "401 response has no WWW-Authenticate header");
500500+ response)
501501+ | None -> response
502502+ end else
503503+ response
504504+418505(* Public request function - executes synchronously with retry support *)
419506let request (T t as wrapped_t) ?headers ?body ?auth ?timeout ?follow_redirects ?max_redirects ~method_ url =
507507+ (* Helper to wrap response with Digest auth handling *)
508508+ let with_digest_handling response =
509509+ handle_digest_auth wrapped_t ~headers ~body ~auth ~timeout ~follow_redirects ~max_redirects ~method_ ~url response
510510+ in
420511 match t.retry with
421512 | None ->
422513 (* No retry configured, execute directly *)
423423- make_request_internal wrapped_t ?headers ?body ?auth ?timeout
424424- ?follow_redirects ?max_redirects ~method_ url
514514+ let response = make_request_internal wrapped_t ?headers ?body ?auth ?timeout
515515+ ?follow_redirects ?max_redirects ~method_ url in
516516+ with_digest_handling response
425517 | Some retry_config ->
426518 (* Wrap in retry logic *)
427519 let should_retry_exn = function
···440532 try
441533 let response = make_request_internal wrapped_t ?headers ?body ?auth ?timeout
442534 ?follow_redirects ?max_redirects ~method_ url in
535535+ (* Handle Digest auth challenge if applicable *)
536536+ let response = with_digest_handling response in
443537 let status = Response.status_code response in
444538445539 (* Check if this status code should be retried *)
···738832 (* Suppress TLS debug output by default *)
739833 set_tls_tracing_level Logs.Warning
740834end
835835+836836+(** {1 Module-Level Convenience Functions}
837837+838838+ These functions perform one-off requests without creating a session.
839839+ They are thin wrappers around {!One} module functions.
840840+ For multiple requests to the same hosts, prefer creating a session with {!create}
841841+ to benefit from connection pooling and cookie persistence. *)
842842+843843+let simple_get ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?auth ?timeout url =
844844+ One.get ~sw ~clock:env#clock ~net:env#net ?headers ?auth ?timeout url
845845+846846+let simple_post ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?body ?auth ?timeout url =
847847+ One.post ~sw ~clock:env#clock ~net:env#net ?headers ?body ?auth ?timeout url
848848+849849+let simple_put ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?body ?auth ?timeout url =
850850+ One.put ~sw ~clock:env#clock ~net:env#net ?headers ?body ?auth ?timeout url
851851+852852+let simple_patch ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?body ?auth ?timeout url =
853853+ One.patch ~sw ~clock:env#clock ~net:env#net ?headers ?body ?auth ?timeout url
854854+855855+let simple_delete ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?auth ?timeout url =
856856+ One.delete ~sw ~clock:env#clock ~net:env#net ?headers ?auth ?timeout url
857857+858858+let simple_head ~sw (env : < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. >) ?headers ?auth ?timeout url =
859859+ One.head ~sw ~clock:env#clock ~net:env#net ?headers ?auth ?timeout url
+83
lib/requests.mli
···228228 ?retry:Retry.config ->
229229 ?persist_cookies:bool ->
230230 ?xdg:Xdge.t ->
231231+ ?auto_decompress:bool ->
231232 < clock: _ Eio.Time.clock; net: _ Eio.Net.t; fs: Eio.Fs.dir_ty Eio.Path.t; .. > ->
232233 t
233234(** Create a new requests instance with persistent state and connection pooling.
···250251 @param retry Retry configuration for failed requests
251252 @param persist_cookies Whether to persist cookies to disk (default: false)
252253 @param xdg XDG directory context for cookies (required if persist_cookies=true)
254254+ @param auto_decompress Whether to automatically decompress gzip/deflate responses (default: true)
253255254256 {b Note:} HTTP caching has been disabled for simplicity. See CACHEIO.md for integration notes
255257 if you need to restore caching functionality in the future.
···695697 Use [Logs.Src.set_level src] to control logging verbosity.
696698 Example: [Logs.Src.set_level Requests.src (Some Logs.Debug)] *)
697699val src : Logs.Src.t
700700+701701+(** {1 Module-Level Convenience Functions}
702702+703703+ These functions perform one-off requests without creating a session.
704704+ They are thin wrappers around {!One} module functions with a simplified
705705+ environment-based interface.
706706+707707+ For multiple requests to the same hosts, prefer creating a session with
708708+ {!create} to benefit from connection pooling and cookie persistence.
709709+710710+ {b Example:}
711711+ {[
712712+ Eio_main.run @@ fun env ->
713713+ Eio.Switch.run @@ fun sw ->
714714+ let response = Requests.simple_get ~sw env "https://example.com" in
715715+ Printf.printf "Status: %d\n" (Response.status_code response)
716716+ ]}
717717+*)
718718+719719+val simple_get :
720720+ sw:Eio.Switch.t ->
721721+ < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > ->
722722+ ?headers:Headers.t ->
723723+ ?auth:Auth.t ->
724724+ ?timeout:Timeout.t ->
725725+ string ->
726726+ Response.t
727727+(** [simple_get ~sw env url] performs a one-off GET request. *)
728728+729729+val simple_post :
730730+ sw:Eio.Switch.t ->
731731+ < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > ->
732732+ ?headers:Headers.t ->
733733+ ?body:Body.t ->
734734+ ?auth:Auth.t ->
735735+ ?timeout:Timeout.t ->
736736+ string ->
737737+ Response.t
738738+(** [simple_post ~sw env url] performs a one-off POST request. *)
739739+740740+val simple_put :
741741+ sw:Eio.Switch.t ->
742742+ < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > ->
743743+ ?headers:Headers.t ->
744744+ ?body:Body.t ->
745745+ ?auth:Auth.t ->
746746+ ?timeout:Timeout.t ->
747747+ string ->
748748+ Response.t
749749+(** [simple_put ~sw env url] performs a one-off PUT request. *)
750750+751751+val simple_patch :
752752+ sw:Eio.Switch.t ->
753753+ < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > ->
754754+ ?headers:Headers.t ->
755755+ ?body:Body.t ->
756756+ ?auth:Auth.t ->
757757+ ?timeout:Timeout.t ->
758758+ string ->
759759+ Response.t
760760+(** [simple_patch ~sw env url] performs a one-off PATCH request. *)
761761+762762+val simple_delete :
763763+ sw:Eio.Switch.t ->
764764+ < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > ->
765765+ ?headers:Headers.t ->
766766+ ?auth:Auth.t ->
767767+ ?timeout:Timeout.t ->
768768+ string ->
769769+ Response.t
770770+(** [simple_delete ~sw env url] performs a one-off DELETE request. *)
771771+772772+val simple_head :
773773+ sw:Eio.Switch.t ->
774774+ < clock: _ Eio.Time.clock; net: _ Eio.Net.t; .. > ->
775775+ ?headers:Headers.t ->
776776+ ?auth:Auth.t ->
777777+ ?timeout:Timeout.t ->
778778+ string ->
779779+ Response.t
780780+(** [simple_head ~sw env url] performs a one-off HEAD request. *)
+23
lib/response.ml
···6666 else
6767 t.body
68686969+let text t =
7070+ if t.closed then
7171+ failwith "Response has been closed"
7272+ else
7373+ Eio.Buf_read.of_flow t.body ~max_size:max_int |> Eio.Buf_read.take_all
7474+7575+let json t =
7676+ let body_str = text t in
7777+ match Jsont_bytesrw.decode_string' Jsont.json body_str with
7878+ | Ok json -> Ok json
7979+ | Error e -> Error (Jsont.Error.to_string e)
8080+8181+let raise_for_status t =
8282+ if t.status >= 400 then
8383+ raise (Error.HTTPError {
8484+ url = t.url;
8585+ status = t.status;
8686+ reason = Status.reason_phrase (Status.of_int t.status);
8787+ body = None;
8888+ headers = t.headers;
8989+ })
9090+ else
9191+ t
69927093(* Pretty printers *)
7194let pp ppf t =
+33
lib/response.mli
···106106 ]}
107107*)
108108109109+val text : t -> string
110110+(** [text response] reads and returns the entire response body as a string.
111111+ The response body is fully consumed by this operation.
112112+113113+ @raise Failure if the response has already been closed. *)
114114+115115+val json : t -> (Jsont.json, string) result
116116+(** [json response] parses the response body as JSON.
117117+ Returns [Ok json] on success or [Error msg] if parsing fails.
118118+ The response body is fully consumed by this operation.
119119+120120+ Example:
121121+ {[
122122+ match Response.json response with
123123+ | Ok json -> process_json json
124124+ | Error msg -> Printf.eprintf "JSON parse error: %s\n" msg
125125+ ]}
126126+127127+ @raise Failure if the response has already been closed. *)
128128+129129+val raise_for_status : t -> t
130130+(** [raise_for_status response] raises {!Error.HTTPError} if the response
131131+ status code indicates an error (>= 400). Returns the response unchanged
132132+ if the status indicates success (< 400).
133133+134134+ This is useful for failing fast on HTTP errors:
135135+ {[
136136+ let response = Requests.get req url |> Response.raise_for_status in
137137+ (* Only reaches here if status < 400 *)
138138+ process_success response
139139+ ]}
140140+141141+ @raise Error.HTTPError if status code >= 400. *)
109142110143(** {1 Pretty Printing} *)
111144