···33333434let custom f = Custom f
35353636+(** Check if a URL uses HTTPS scheme *)
3737+let is_https url =
3838+ let uri = Uri.of_string url in
3939+ match Uri.scheme uri with
4040+ | Some "https" -> true
4141+ | _ -> false
4242+4343+(** Get the authentication type name for error messages *)
4444+let auth_type_name = function
4545+ | No_auth -> "None"
4646+ | Basic _ -> "Basic"
4747+ | Bearer _ -> "Bearer"
4848+ | Digest _ -> "Digest"
4949+ | Custom _ -> "Custom"
5050+5151+(** Check if auth type requires HTTPS (per RFC 7617/6750).
5252+ Basic, Bearer, and Digest send credentials that can be intercepted. *)
5353+let requires_https = function
5454+ | Basic _ | Bearer _ | Digest _ -> true
5555+ | No_auth | Custom _ -> false
5656+5757+(** Validate that sensitive authentication is used over HTTPS.
5858+ Per RFC 7617 Section 4 (Basic) and RFC 6750 Section 5.1 (Bearer):
5959+ These authentication methods MUST be used over TLS to prevent credential leakage.
6060+6161+ @param allow_insecure_auth If true, skip the check (for testing environments)
6262+ @param url The request URL
6363+ @param auth The authentication configuration
6464+ @raise Error.Insecure_auth if auth requires HTTPS but URL is HTTP *)
6565+let validate_secure_transport ?(allow_insecure_auth = false) ~url auth =
6666+ if allow_insecure_auth then
6767+ Log.warn (fun m -> m "allow_insecure_auth=true: skipping HTTPS check for %s auth"
6868+ (auth_type_name auth))
6969+ else if requires_https auth && not (is_https url) then begin
7070+ Log.err (fun m -> m "%s authentication rejected over HTTP (use HTTPS or allow_insecure_auth=true)"
7171+ (auth_type_name auth));
7272+ raise (Error.err (Error.Insecure_auth {
7373+ url;
7474+ auth_type = auth_type_name auth
7575+ }))
7676+ end
7777+3678let apply auth headers =
3779 match auth with
3880 | No_auth -> headers
···4991 | Custom f ->
5092 Log.debug (fun m -> m "Applying custom authentication handler");
5193 f headers
9494+9595+(** Apply authentication with HTTPS validation.
9696+ This is the secure version that checks transport security before applying auth.
9797+9898+ @param allow_insecure_auth If true, allow auth over HTTP (not recommended)
9999+ @param url The request URL (used for security check)
100100+ @param auth The authentication to apply
101101+ @param headers The headers to modify *)
102102+let apply_secure ?(allow_insecure_auth = false) ~url auth headers =
103103+ validate_secure_transport ~allow_insecure_auth ~url auth;
104104+ apply auth headers
5210553106(** {1 Digest Authentication Implementation} *)
54107
+23-1
lib/auth.mli
···3333(** Custom authentication handler *)
34343535val apply : t -> Headers.t -> Headers.t
3636-(** Apply authentication to headers *)
3636+(** Apply authentication to headers.
3737+ Note: This does not validate transport security. Use [apply_secure] for
3838+ HTTPS enforcement per RFC 7617/6750. *)
3939+4040+val apply_secure : ?allow_insecure_auth:bool -> url:string -> t -> Headers.t -> Headers.t
4141+(** Apply authentication with HTTPS validation.
4242+ Per RFC 7617 Section 4 (Basic) and RFC 6750 Section 5.1 (Bearer):
4343+ Basic, Bearer, and Digest authentication MUST be used over TLS.
4444+4545+ @param allow_insecure_auth If [true], skip the HTTPS check (not recommended,
4646+ only for testing environments). Default: [false]
4747+ @param url The request URL (used for security check)
4848+ @raise Error.Insecure_auth if sensitive auth is used over HTTP *)
4949+5050+val validate_secure_transport : ?allow_insecure_auth:bool -> url:string -> t -> unit
5151+(** Validate that sensitive authentication would be safe to use.
5252+ Raises [Error.Insecure_auth] if Basic/Bearer/Digest auth would be used over HTTP.
5353+5454+ @param allow_insecure_auth If [true], skip the check. Default: [false] *)
5555+5656+val requires_https : t -> bool
5757+(** Returns [true] if the authentication type requires HTTPS transport.
5858+ Basic, Bearer, and Digest require HTTPS; No_auth and Custom do not. *)
37593860(** {1 Digest Authentication Support} *)
3961
+10
lib/error.ml
···4545 | Headers_too_large of { limit: int; actual: int }
4646 | Decompression_bomb of { limit: int64; ratio: float }
4747 | Content_length_mismatch of { expected: int64; actual: int64 }
4848+ | Insecure_auth of { url: string; auth_type: string }
4949+ (** Per RFC 7617 Section 4 and RFC 6750 Section 5.1:
5050+ Basic, Bearer, and Digest authentication over unencrypted HTTP
5151+ exposes credentials to eavesdropping. *)
48524953 (* JSON errors *)
5054 | Json_parse_error of { body_preview: string; reason: string }
···137141 Format.fprintf ppf "Content-Length mismatch: expected %Ld bytes, received %Ld bytes"
138142 expected actual
139143144144+ | Insecure_auth { url; auth_type } ->
145145+ Format.fprintf ppf "%s authentication over unencrypted HTTP rejected for %s. \
146146+ Use HTTPS or set allow_insecure_auth=true (not recommended)"
147147+ auth_type (sanitize_url url)
148148+140149 | Json_parse_error { body_preview; reason } ->
141150 let preview = if String.length body_preview > 100
142151 then String.sub body_preview 0 100 ^ "..."
···229238 | Headers_too_large _ -> true
230239 | Decompression_bomb _ -> true
231240 | Invalid_redirect _ -> true
241241+ | Insecure_auth _ -> true
232242 | _ -> false
233243234244let is_json_error = function
+5
lib/error.mli
···6767 | Headers_too_large of { limit: int; actual: int }
6868 | Decompression_bomb of { limit: int64; ratio: float }
6969 | Content_length_mismatch of { expected: int64; actual: int64 }
7070+ | Insecure_auth of { url: string; auth_type: string }
7171+ (** Per RFC 7617 Section 4 and RFC 6750 Section 5.1:
7272+ Basic, Bearer, and Digest authentication over unencrypted HTTP
7373+ exposes credentials to eavesdropping. Raised when attempting
7474+ to use these auth methods over HTTP without explicit opt-out. *)
70757176 (* JSON errors *)
7277 | Json_parse_error of { body_preview: string; reason: string }
+6-3
lib/http_client.ml
···177177178178 (* Read response using Buf_read *)
179179 let buf_read = Http_read.of_flow flow ~max_size:max_int in
180180- Http_read.response ~limits buf_read
180180+ let (_version, status, headers, body) = Http_read.response ~limits buf_read in
181181+ (status, headers, body)
181182182183(** Make HTTP request with optional auto-decompression *)
183184let make_request_decompress ?(limits=default_limits) ~sw ~method_ ~uri ~headers ~body ~auto_decompress flow =
···335336336337 (* Read final response *)
337338 let buf_read = Http_read.of_flow flow ~max_size:max_int in
338338- Http_read.response ~limits buf_read
339339+ let (_version, status, headers, body) = Http_read.response ~limits buf_read in
340340+ (status, headers, body)
339341340342 | Rejected (status, resp_headers, resp_body_str) ->
341343 (* Server rejected - return error response without sending body *)
···359361360362 (* Read response *)
361363 let buf_read = Http_read.of_flow flow ~max_size:max_int in
362362- Http_read.response ~limits buf_read
364364+ let (_version, status, headers, body) = Http_read.response ~limits buf_read in
365365+ (status, headers, body)
363366 end
364367365368(** Make HTTP request with 100-continue support and optional auto-decompression *)
+47-16
lib/http_read.ml
···6767let reason_phrase r =
6868 Read.line r
69697070+(** {1 HTTP Version Type}
7171+7272+ Per Recommendation #26: Expose HTTP version used for the response *)
7373+7474+type http_version =
7575+ | HTTP_1_0
7676+ | HTTP_1_1
7777+7878+let http_version_of_string = function
7979+ | "HTTP/1.0" -> HTTP_1_0
8080+ | "HTTP/1.1" -> HTTP_1_1
8181+ | v -> raise (Error.err (Error.Invalid_request {
8282+ reason = "Invalid HTTP version: " ^ v
8383+ }))
8484+8585+let http_version_to_string = function
8686+ | HTTP_1_0 -> "HTTP/1.0"
8787+ | HTTP_1_1 -> "HTTP/1.1"
8888+7089(** {1 Status Line Parser} *)
71907291let status_line r =
7373- let version = http_version r in
7474- (* Validate HTTP version *)
7575- (match version with
7676- | "HTTP/1.1" | "HTTP/1.0" -> ()
7777- | _ ->
7878- raise (Error.err (Error.Invalid_request {
7979- reason = "Invalid HTTP version: " ^ version
8080- })));
9292+ let version_str = http_version r in
9393+ (* Parse and validate HTTP version *)
9494+ let version = http_version_of_string version_str in
8195 sp r;
8296 let code = status_code r in
8397 sp r;
8498 let _reason = reason_phrase r in
8585- Log.debug (fun m -> m "Parsed status line: %s %d" version code);
8686- code
9999+ Log.debug (fun m -> m "Parsed status line: %s %d" version_str code);
100100+ (version, code)
8710188102(** {1 Header Parsing} *)
89103···364378365379(** Parse complete response (status + headers + body) to string *)
366380let response ~limits r =
367367- let status = status_line r in
381381+ let (version, status) = status_line r in
368382 let hdrs = headers ~limits r in
369383370384 (* Determine how to read body *)
371385 let transfer_encoding = Headers.get "transfer-encoding" hdrs in
372386 let content_length = Headers.get "content-length" hdrs |> Option.map Int64.of_string in
373387388388+ (* Per RFC 9112 Section 6.3: When both Transfer-Encoding and Content-Length
389389+ are present, Transfer-Encoding takes precedence. The presence of both
390390+ headers is a potential HTTP request smuggling attack indicator. *)
374391 let body = match transfer_encoding, content_length with
375375- | Some te, _ when String.lowercase_ascii te |> String.trim = "chunked" ->
392392+ | Some te, Some _ when String.lowercase_ascii te |> String.trim = "chunked" ->
393393+ (* Both headers present - log warning per RFC 9112 Section 6.3 *)
394394+ Log.warn (fun m -> m "Both Transfer-Encoding and Content-Length present - \
395395+ ignoring Content-Length per RFC 9112 (potential attack indicator)");
396396+ chunked_body ~limits r
397397+ | Some te, None when String.lowercase_ascii te |> String.trim = "chunked" ->
376398 Log.debug (fun m -> m "Reading chunked response body");
377399 chunked_body ~limits r
378400 | _, Some len ->
···386408 ""
387409 in
388410389389- (status, hdrs, body)
411411+ (version, status, hdrs, body)
390412391413(** Response with streaming body *)
392414type stream_response = {
415415+ http_version : http_version;
393416 status : int;
394417 headers : Headers.t;
395418 body : [ `String of string
···398421}
399422400423let response_stream ~limits r =
401401- let status = status_line r in
424424+ let (version, status) = status_line r in
402425 let hdrs = headers ~limits r in
403426404427 (* Determine body type *)
405428 let transfer_encoding = Headers.get "transfer-encoding" hdrs in
406429 let content_length = Headers.get "content-length" hdrs |> Option.map Int64.of_string in
407430431431+ (* Per RFC 9112 Section 6.3: When both Transfer-Encoding and Content-Length
432432+ are present, Transfer-Encoding takes precedence. The presence of both
433433+ headers is a potential HTTP request smuggling attack indicator. *)
408434 let body = match transfer_encoding, content_length with
409409- | Some te, _ when String.lowercase_ascii te |> String.trim = "chunked" ->
435435+ | Some te, Some _ when String.lowercase_ascii te |> String.trim = "chunked" ->
436436+ (* Both headers present - log warning per RFC 9112 Section 6.3 *)
437437+ Log.warn (fun m -> m "Both Transfer-Encoding and Content-Length present - \
438438+ ignoring Content-Length per RFC 9112 (potential attack indicator)");
439439+ `Stream (chunked_body_stream ~limits r)
440440+ | Some te, None when String.lowercase_ascii te |> String.trim = "chunked" ->
410441 Log.debug (fun m -> m "Creating chunked body stream");
411442 `Stream (chunked_body_stream ~limits r)
412443 | _, Some len ->
···420451 `None
421452 in
422453423423- { status; headers = hdrs; body }
454454+ { http_version = version; status; headers = hdrs; body }
424455425456(** {1 Convenience Functions} *)
426457
+24-5
lib/http_read.mli
···2121type limits = Response_limits.t
2222(** Alias for {!Response_limits.t}. See {!Response_limits} for documentation. *)
23232424+(** {1 HTTP Version Type}
2525+2626+ Per Recommendation #26: Expose HTTP version used for the response. *)
2727+2828+type http_version =
2929+ | HTTP_1_0 (** HTTP/1.0 *)
3030+ | HTTP_1_1 (** HTTP/1.1 *)
3131+(** HTTP protocol version. Useful for debugging protocol negotiation
3232+ and monitoring HTTP/2 adoption (when supported). *)
3333+3434+val http_version_to_string : http_version -> string
3535+(** [http_version_to_string v] returns "HTTP/1.0" or "HTTP/1.1". *)
3636+2437(** {1 Low-level Parsers} *)
25382639val http_version : Eio.Buf_read.t -> string
···3043(** [status_code r] parses a 3-digit HTTP status code.
3144 @raise Error.t if the status code is invalid. *)
32453333-val status_line : Eio.Buf_read.t -> int
3434-(** [status_line r] parses a complete HTTP status line and returns the status code.
4646+val status_line : Eio.Buf_read.t -> http_version * int
4747+(** [status_line r] parses a complete HTTP status line and returns
4848+ the HTTP version and status code as a tuple.
3549 Validates that the HTTP version is 1.0 or 1.1.
3650 @raise Error.t if the status line is invalid. *)
3751···74887589(** {1 High-level Response Parsing} *)
76907777-val response : limits:limits -> Eio.Buf_read.t -> int * Headers.t * string
9191+val response : limits:limits -> Eio.Buf_read.t -> http_version * int * Headers.t * string
7892(** [response ~limits r] parses a complete HTTP response including:
7979- - Status line (returns status code)
9393+ - HTTP version
9494+ - Status code
8095 - Headers
8196 - Body (based on Transfer-Encoding or Content-Length)
82979898+ Returns [(http_version, status, headers, body)].
9999+83100 This reads the entire body into memory. For large responses,
84101 use {!response_stream} instead. *)
8510286103(** {1 Streaming Response} *)
8710488105type stream_response = {
106106+ http_version : http_version; (** HTTP protocol version *)
89107 status : int;
90108 headers : Headers.t;
91109 body : [ `String of string
92110 | `Stream of Eio.Flow.source_ty Eio.Resource.t
93111 | `None ]
94112}
9595-(** A parsed response with optional streaming body. *)
113113+(** A parsed response with optional streaming body.
114114+ Per Recommendation #26: Includes HTTP version for debugging/monitoring. *)
9611597116val response_stream : limits:limits -> Eio.Buf_read.t -> stream_response
98117(** [response_stream ~limits r] parses status line and headers, then
···7575 ?min_tls_version:tls_version ->
7676 ?expect_100_continue:bool ->
7777 ?expect_100_continue_threshold:int64 ->
7878+ ?allow_insecure_auth:bool ->
7879 method_:Method.t ->
7980 string ->
8081 Response.t
8182(** [request ~sw ~clock ~net ?headers ?body ?auth ?timeout ?follow_redirects
8283 ?max_redirects ?verify_tls ?tls_config ?auto_decompress ?min_tls_version
8383- ?expect_100_continue ?expect_100_continue_threshold ~method_ url]
8484+ ?expect_100_continue ?expect_100_continue_threshold ?allow_insecure_auth
8585+ ~method_ url]
8486 makes a single HTTP request without connection pooling.
85878688 Each call opens a new TCP connection (with TLS if https://), makes the
···101103 @param min_tls_version Minimum TLS version to accept (default: TLS_1_2)
102104 @param expect_100_continue Use HTTP 100-continue for large bodies (default: true)
103105 @param expect_100_continue_threshold Body size threshold to trigger 100-continue (default: 1MB)
106106+ @param allow_insecure_auth Allow Basic/Bearer/Digest auth over HTTP (default: false).
107107+ Per RFC 7617 Section 4 and RFC 6750 Section 5.1, these auth methods
108108+ MUST be used over TLS. Set to [true] only for testing environments.
104109 @param method_ HTTP method (GET, POST, etc.)
105110 @param url URL to request
106111*)
···117122 ?verify_tls:bool ->
118123 ?tls_config:Tls.Config.client ->
119124 ?min_tls_version:tls_version ->
125125+ ?allow_insecure_auth:bool ->
120126 string ->
121127 Response.t
122128(** GET request. See {!request} for parameter details. *)
···134140 ?min_tls_version:tls_version ->
135141 ?expect_100_continue:bool ->
136142 ?expect_100_continue_threshold:int64 ->
143143+ ?allow_insecure_auth:bool ->
137144 string ->
138145 Response.t
139146(** POST request with 100-continue support. See {!request} for parameter details. *)
···151158 ?min_tls_version:tls_version ->
152159 ?expect_100_continue:bool ->
153160 ?expect_100_continue_threshold:int64 ->
161161+ ?allow_insecure_auth:bool ->
154162 string ->
155163 Response.t
156164(** PUT request with 100-continue support. See {!request} for parameter details. *)
···165173 ?verify_tls:bool ->
166174 ?tls_config:Tls.Config.client ->
167175 ?min_tls_version:tls_version ->
176176+ ?allow_insecure_auth:bool ->
168177 string ->
169178 Response.t
170179(** DELETE request. See {!request} for parameter details. *)
···179188 ?verify_tls:bool ->
180189 ?tls_config:Tls.Config.client ->
181190 ?min_tls_version:tls_version ->
191191+ ?allow_insecure_auth:bool ->
182192 string ->
183193 Response.t
184194(** HEAD request. See {!request} for parameter details. *)
···196206 ?min_tls_version:tls_version ->
197207 ?expect_100_continue:bool ->
198208 ?expect_100_continue_threshold:int64 ->
209209+ ?allow_insecure_auth:bool ->
199210 string ->
200211 Response.t
201212(** PATCH request with 100-continue support. See {!request} for parameter details. *)
···216227 ?min_tls_version:tls_version ->
217228 ?expect_100_continue:bool ->
218229 ?expect_100_continue_threshold:int64 ->
230230+ ?allow_insecure_auth:bool ->
219231 source:Eio.Flow.source_ty Eio.Resource.t ->
220232 string ->
221233 Response.t
···233245 ?verify_tls:bool ->
234246 ?tls_config:Tls.Config.client ->
235247 ?min_tls_version:tls_version ->
248248+ ?allow_insecure_auth:bool ->
236249 string ->
237250 sink:Eio.Flow.sink_ty Eio.Resource.t ->
238251 unit
+9
lib/requests.ml
···2424module Cache_control = Cache_control
2525module Response_limits = Response_limits
2626module Expect_continue = Expect_continue
2727+module Version = Version
27282829(** Minimum TLS version configuration.
2930 Per Recommendation #6: Allow enforcing minimum TLS version. *)
···292293 let headers = match headers with
293294 | Some h -> Headers.merge t.default_headers h
294295 | None -> t.default_headers
296296+ in
297297+298298+ (* Add default User-Agent if not already set - per RFC 9110 Section 10.1.5 *)
299299+ let headers =
300300+ if not (Headers.mem "User-Agent" headers) then
301301+ Headers.set "User-Agent" Version.user_agent headers
302302+ else
303303+ headers
295304 in
296305297306 (* Use provided auth or default *)
+15
lib/response.ml
···193193 else
194194 t
195195196196+(** Result-based status check - per Recommendation #21.
197197+ Returns Ok response for 2xx success, Error for 4xx/5xx errors.
198198+ Enables functional error handling without exceptions. *)
199199+let check_status t =
200200+ if t.status >= 400 then
201201+ Error (Error.Http_error {
202202+ url = t.url;
203203+ status = t.status;
204204+ reason = Status.reason_phrase (Status.of_int t.status);
205205+ body_preview = None;
206206+ headers = Headers.to_list t.headers;
207207+ })
208208+ else
209209+ Ok t
210210+196211(* Pretty printers *)
197212let pp ppf t =
198213 Format.fprintf ppf "@[<v>Response:@,\
+16
lib/response.mli
···236236237237 @raise Error.HTTPError if status code >= 400. *)
238238239239+val check_status : t -> (t, Error.error) result
240240+(** [check_status response] returns [Ok response] if the status code is < 400,
241241+ or [Error error] if the status code indicates an error (>= 400).
242242+243243+ This provides functional error handling without exceptions, complementing
244244+ {!raise_for_status} for different coding styles.
245245+246246+ Example:
247247+ {[
248248+ match Response.check_status response with
249249+ | Ok resp -> process_success resp
250250+ | Error err -> handle_error err
251251+ ]}
252252+253253+ Per Recommendation #21: Provides a Result-based alternative to raise_for_status. *)
254254+239255(** {1 Pretty Printing} *)
240256241257val pp : Format.formatter -> t -> unit
+31-18
lib/retry.ml
···7070 attempt base_delay config.jitter final_delay);
7171 final_delay
72727373-let parse_retry_after value =
7373+(** Parse Retry-After header and cap to backoff_max to prevent DoS.
7474+ Per RFC 9110 Section 10.2.3 and Recommendation #5:
7575+ Cap server-specified Retry-After values to prevent malicious servers
7676+ from causing indefinite client blocking. *)
7777+let parse_retry_after ?(backoff_max = 120.0) value =
7478 Log.debug (fun m -> m "Parsing Retry-After header: %s" value);
75797676- (* First try to parse as integer (delay in seconds) *)
7777- match int_of_string_opt value with
7878- | Some seconds ->
7979- Log.debug (fun m -> m "Retry-After is %d seconds" seconds);
8080- Some (float_of_int seconds)
8181- | None ->
8282- (* Try to parse as HTTP date (RFC 3339 format) *)
8383- match Ptime.of_rfc3339 value with
8484- | Ok (time, _tz_offset, _tz_string) ->
8585- let now = Unix.time () in
8686- let target = Ptime.to_float_s time in
8787- let delay = max 0.0 (target -. now) in
8888- Log.debug (fun m -> m "Retry-After is HTTP date, delay=%.2f seconds" delay);
8989- Some delay
9090- | Error _ ->
9191- Log.warn (fun m -> m "Failed to parse Retry-After header: %s" value);
9292- None
8080+ let raw_delay =
8181+ (* First try to parse as integer (delay in seconds) *)
8282+ match int_of_string_opt value with
8383+ | Some seconds ->
8484+ Log.debug (fun m -> m "Retry-After is %d seconds" seconds);
8585+ Some (float_of_int seconds)
8686+ | None ->
8787+ (* Try to parse as HTTP date (RFC 3339 format) *)
8888+ match Ptime.of_rfc3339 value with
8989+ | Ok (time, _tz_offset, _tz_string) ->
9090+ let now = Unix.time () in
9191+ let target = Ptime.to_float_s time in
9292+ let delay = max 0.0 (target -. now) in
9393+ Log.debug (fun m -> m "Retry-After is HTTP date, delay=%.2f seconds" delay);
9494+ Some delay
9595+ | Error _ ->
9696+ Log.warn (fun m -> m "Failed to parse Retry-After header: %s" value);
9797+ None
9898+ in
9999+ (* Cap to backoff_max to prevent DoS from malicious Retry-After values *)
100100+ match raw_delay with
101101+ | Some delay when delay > backoff_max ->
102102+ Log.warn (fun m -> m "Retry-After delay %.2fs exceeds backoff_max %.2fs, capping"
103103+ delay backoff_max);
104104+ Some backoff_max
105105+ | other -> other
9310694107let with_retry ~sw:_ ~clock ~config ~f ~should_retry_exn =
95108 let rec attempt_with_retry attempt =
+5-2
lib/retry.mli
···4141(** Calculate backoff delay for a given attempt *)
4242val calculate_backoff : config:config -> attempt:int -> float
43434444-(** Parse Retry-After header value (seconds or HTTP date) *)
4545-val parse_retry_after : string -> float option
4444+(** Parse Retry-After header value (seconds or HTTP date).
4545+ Per RFC 9110 Section 10.2.3 and Recommendation #5:
4646+ Values are capped to [backoff_max] (default 120s) to prevent DoS
4747+ from malicious servers specifying extremely long delays. *)
4848+val parse_retry_after : ?backoff_max:float -> string -> float option
46494750(** Execute a request with retry logic *)
4851val with_retry :
+20
lib/version.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Library version and User-Agent header support.
77+ Per RFC 9110 Section 10.1.5 and Recommendation #30:
88+ Set a default User-Agent to help server-side debugging. *)
99+1010+(** Library version - update this when releasing new versions *)
1111+let version = "0.1.0"
1212+1313+(** Library name *)
1414+let name = "ocaml-requests"
1515+1616+(** Default User-Agent header value.
1717+ Format follows common conventions: library-name/version (runtime-info)
1818+ Per RFC 9110 Section 10.1.5, this helps with debugging and statistics. *)
1919+let user_agent =
2020+ Printf.sprintf "%s/%s (OCaml %s)" name version Sys.ocaml_version
+23
lib/version.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Library version and User-Agent header support.
77+ Per RFC 9110 Section 10.1.5 and Recommendation #30:
88+ Provides a default User-Agent header for HTTP requests. *)
99+1010+val version : string
1111+(** Library version string (e.g., "0.1.0") *)
1212+1313+val name : string
1414+(** Library name ("ocaml-requests") *)
1515+1616+val user_agent : string
1717+(** Default User-Agent header value.
1818+ Format: "ocaml-requests/VERSION (OCaml OCAML_VERSION)"
1919+ Example: "ocaml-requests/0.1.0 (OCaml 5.2.0)"
2020+2121+ Per RFC 9110 Section 10.1.5, this helps server-side debugging
2222+ and monitoring. The User-Agent is automatically added to requests
2323+ unless the user provides their own. *)