···228228 ~follow_redirects ~max_redirects ~allow_insecure_auth ?timeout:timeout_obj env in
229229230230 (* Set authentication if provided *)
231231- let req = match auth with
232232- | Some auth_str ->
233233- (match parse_auth auth_str with
234234- | Some (user, pass) ->
235235- Requests.set_auth req
236236- (Requests.Auth.basic ~username:user ~password:pass)
237237- | None ->
238238- Logs.warn (fun m -> m "Invalid auth format, ignoring");
239239- req)
240240- | None -> req
231231+ let req = match Option.bind auth parse_auth with
232232+ | Some (user, pass) ->
233233+ Requests.set_auth req (Requests.Auth.basic ~username:user ~password:pass)
234234+ | None ->
235235+ (if Option.is_some auth then
236236+ Logs.warn (fun m -> m "Invalid auth format, ignoring"));
237237+ req
241238 in
242239243240 (* Build headers from command line *)
+15-37
lib/http_client.ml
···166166167167(** {1 Request Execution} *)
168168169169+(** Apply auto-decompression to response if enabled *)
170170+let maybe_decompress ~limits ~auto_decompress (status, resp_headers, body_str) =
171171+ match auto_decompress, Headers.get "content-encoding" resp_headers with
172172+ | true, Some encoding ->
173173+ let body_str = decompress_body ~limits ~content_encoding:encoding body_str in
174174+ let resp_headers = Headers.remove "content-encoding" resp_headers in
175175+ (status, resp_headers, body_str)
176176+ | _ ->
177177+ (status, resp_headers, body_str)
178178+169179(** Make HTTP request over a pooled connection using Buf_write/Buf_read *)
170180let make_request ?(limits=default_limits) ~sw ~method_ ~uri ~headers ~body flow =
171181 Log.debug (fun m -> m "Making %s request to %s" (Method.to_string method_) (Uri.to_string uri));
···182192183193(** Make HTTP request with optional auto-decompression *)
184194let make_request_decompress ?(limits=default_limits) ~sw ~method_ ~uri ~headers ~body ~auto_decompress flow =
185185- let (status, resp_headers, body_str) = make_request ~limits ~sw ~method_ ~uri ~headers ~body flow in
186186- if auto_decompress then
187187- let body_str = match Headers.get "content-encoding" resp_headers with
188188- | Some encoding -> decompress_body ~limits ~content_encoding:encoding body_str
189189- | None -> body_str
190190- in
191191- (* Remove Content-Encoding header after decompression since body is now uncompressed *)
192192- let resp_headers = match Headers.get "content-encoding" resp_headers with
193193- | Some _ -> Headers.remove "content-encoding" resp_headers
194194- | None -> resp_headers
195195- in
196196- (status, resp_headers, body_str)
197197- else
198198- (status, resp_headers, body_str)
195195+ make_request ~limits ~sw ~method_ ~uri ~headers ~body flow
196196+ |> maybe_decompress ~limits ~auto_decompress
199197200198(** {1 HTTP 100-Continue Protocol Implementation}
201199···369367let make_request_100_continue_decompress
370368 ?(limits=default_limits)
371369 ?(expect_100=default_expect_100_config)
372372- ~clock
373373- ~sw
374374- ~method_
375375- ~uri
376376- ~headers
377377- ~body
378378- ~auto_decompress
379379- flow =
380380- let (status, resp_headers, body_str) =
381381- make_request_100_continue ~limits ~expect_100 ~clock ~sw ~method_ ~uri ~headers ~body flow
382382- in
383383- if auto_decompress then
384384- let body_str = match Headers.get "content-encoding" resp_headers with
385385- | Some encoding -> decompress_body ~limits ~content_encoding:encoding body_str
386386- | None -> body_str
387387- in
388388- let resp_headers = match Headers.get "content-encoding" resp_headers with
389389- | Some _ -> Headers.remove "content-encoding" resp_headers
390390- | None -> resp_headers
391391- in
392392- (status, resp_headers, body_str)
393393- else
394394- (status, resp_headers, body_str)
370370+ ~clock ~sw ~method_ ~uri ~headers ~body ~auto_decompress flow =
371371+ make_request_100_continue ~limits ~expect_100 ~clock ~sw ~method_ ~uri ~headers ~body flow
372372+ |> maybe_decompress ~limits ~auto_decompress
+40-33
lib/http_read.ml
···376376377377(** {1 High-level Response Parsing} *)
378378379379-(** Check if response should have no body per RFC 9110.
380380- Per RFC 9110 Section 6.4.1:
381381- - Any response to a HEAD request
382382- - Any 1xx (Informational) response
383383- - 204 (No Content) response
384384- - 304 (Not Modified) response *)
379379+(** Check if response should have no body per
380380+ {{:https://datatracker.ietf.org/doc/html/rfc9110#section-6.4.1}RFC 9110 Section 6.4.1}:
381381+ {ul
382382+ {- Any response to a HEAD request}
383383+ {- 2xx (Successful) response to a CONNECT request (switches to tunnel mode)}
384384+ {- Any 1xx (Informational) response}
385385+ {- 204 (No Content) response}
386386+ {- 304 (Not Modified) response}} *)
385387let response_has_no_body ~method_ ~status =
386386- (method_ = Some `HEAD) ||
387387- (status >= 100 && status < 200) ||
388388- status = 204 ||
389389- status = 304
388388+ match method_, status with
389389+ | Some `HEAD, _ -> true
390390+ | Some `CONNECT, s when s >= 200 && s < 300 -> true
391391+ | _, s when s >= 100 && s < 200 -> true
392392+ | _, 204 | _, 304 -> true
393393+ | _ -> false
390394391391-(** Parse complete response (status + headers + body) to string *)
395395+(** Helper to normalize and check transfer-encoding *)
396396+let is_chunked_encoding = function
397397+ | None -> false
398398+ | Some te -> String.lowercase_ascii te |> String.trim = "chunked"
399399+400400+(** Parse complete response (status + headers + body) to string.
401401+ Per {{:https://datatracker.ietf.org/doc/html/rfc9112#section-6}RFC 9112 Section 6}}. *)
392402let response ~limits ?method_ r =
393393- let (version, status) = status_line r in
403403+ let version, status = status_line r in
394404 let hdrs = headers ~limits r in
395405396406 (* Per RFC 9110 Section 6.4.1: Certain responses MUST NOT have a body *)
397397- if response_has_no_body ~method_ ~status then begin
398398- Log.debug (fun m -> m "Response has no body (HEAD, 1xx, 204, or 304)");
407407+ if response_has_no_body ~method_ ~status then (
408408+ Log.debug (fun m -> m "Response has no body (HEAD, CONNECT 2xx, 1xx, 204, or 304)");
399409 (version, status, hdrs, "")
400400- end else begin
401401- (* Determine how to read body *)
410410+ ) else
411411+ (* Determine how to read body based on headers.
412412+ Per RFC 9112 Section 6.3: Transfer-Encoding takes precedence over Content-Length *)
402413 let transfer_encoding = Headers.get "transfer-encoding" hdrs in
403414 let content_length = Headers.get "content-length" hdrs |> Option.map Int64.of_string in
404404-405405- (* Per RFC 9112 Section 6.3: When both Transfer-Encoding and Content-Length
406406- are present, Transfer-Encoding takes precedence. The presence of both
407407- headers is a potential HTTP request smuggling attack indicator. *)
408408- let body = match transfer_encoding, content_length with
409409- | Some te, Some _ when String.lowercase_ascii te |> String.trim = "chunked" ->
410410- (* Both headers present - log warning per RFC 9112 Section 6.3 *)
415415+ let body = match is_chunked_encoding transfer_encoding, content_length with
416416+ | true, Some _ ->
417417+ (* Both headers present - potential HTTP request smuggling indicator *)
411418 Log.warn (fun m -> m "Both Transfer-Encoding and Content-Length present - \
412419 ignoring Content-Length per RFC 9112 (potential attack indicator)");
413420 chunked_body ~limits r
414414- | Some te, None when String.lowercase_ascii te |> String.trim = "chunked" ->
421421+ | true, None ->
415422 Log.debug (fun m -> m "Reading chunked response body");
416423 chunked_body ~limits r
417417- | _, Some len ->
424424+ | false, Some len ->
418425 Log.debug (fun m -> m "Reading fixed-length response body (%Ld bytes)" len);
419426 fixed_body ~limits ~length:len r
420420- | Some other_te, None ->
421421- Log.warn (fun m -> m "Unsupported transfer-encoding: %s, assuming no body" other_te);
422422- ""
423423- | None, None ->
424424- Log.debug (fun m -> m "No body indicated");
425425- ""
427427+ | false, None ->
428428+ (match transfer_encoding with
429429+ | Some te ->
430430+ Log.warn (fun m -> m "Unsupported transfer-encoding: %s, assuming no body" te);
431431+ ""
432432+ | None ->
433433+ Log.debug (fun m -> m "No body indicated");
434434+ "")
426435 in
427427-428436 (version, status, hdrs, body)
429429- end
430437431438(** Response with streaming body *)
432439type stream_response = {
+7-3
lib/http_read.mli
···97979898 Returns [(http_version, status, headers, body)].
9999100100- @param method_ The HTTP method of the request. When [`HEAD], the body
101101- is always empty regardless of Content-Length header (per RFC 9110
102102- Section 9.3.2). Similarly for 1xx, 204, and 304 responses.
100100+ @param method_ The HTTP method of the request. Per
101101+ {{:https://datatracker.ietf.org/doc/html/rfc9110#section-6.4.1}RFC 9110 Section 6.4.1},
102102+ certain responses have no body:
103103+ {ul
104104+ {- [`HEAD] - body is always empty regardless of Content-Length}
105105+ {- [`CONNECT] with 2xx - switches to tunnel mode, no body}
106106+ {- 1xx, 204, 304 - no content responses}}
103107104108 This reads the entire body into memory. For large responses,
105109 use {!response_stream} instead. *)
+5-10
lib/one.ml
···265265 (* Read response *)
266266 let limits = Response_limits.default in
267267 let buf_read = Http_read.of_flow ~max_size:65536 flow in
268268- let (_version, status, resp_headers, body_str) =
268268+ let _version, status, resp_headers, body_str =
269269 Http_read.response ~limits ~method_ buf_read in
270270 (* Handle decompression if enabled *)
271271- let body_str =
272272- if auto_decompress then
273273- match Headers.get "content-encoding" resp_headers with
274274- | Some encoding ->
275275- Http_client.decompress_body
276276- ~limits:Response_limits.default
277277- ~content_encoding:encoding body_str
278278- | None -> body_str
279279- else body_str
271271+ let body_str = match auto_decompress, Headers.get "content-encoding" resp_headers with
272272+ | true, Some encoding ->
273273+ Http_client.decompress_body ~limits ~content_encoding:encoding body_str
274274+ | _ -> body_str
280275 in
281276 (status, resp_headers, body_str)
282277
+5-10
lib/requests.ml
···604604 (* Read response *)
605605 let limits = Response_limits.default in
606606 let buf_read = Http_read.of_flow ~max_size:65536 flow in
607607- let (_version, status, resp_headers, body_str) =
607607+ let _version, status, resp_headers, body_str =
608608 Http_read.response ~limits ~method_ buf_read in
609609 (* Handle decompression if enabled *)
610610- let body_str =
611611- if t.auto_decompress then
612612- match Headers.get "content-encoding" resp_headers with
613613- | Some encoding ->
614614- Http_client.decompress_body
615615- ~limits:Response_limits.default
616616- ~content_encoding:encoding body_str
617617- | None -> body_str
618618- else body_str
610610+ let body_str = match t.auto_decompress, Headers.get "content-encoding" resp_headers with
611611+ | true, Some encoding ->
612612+ Http_client.decompress_body ~limits ~content_encoding:encoding body_str
613613+ | _ -> body_str
619614 in
620615 (status, resp_headers, body_str)
621616 )