A batteries included HTTP/1.1 client in OCaml
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** HTTP request retry logic with exponential backoff
7
8 This module provides configurable retry logic for HTTP requests,
9 including exponential backoff, custom retry predicates, and
10 Retry-After header support per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-10.2.3}RFC 9110 Section 10.2.3}.
11
12 {2 Custom Retry Predicates}
13
14 Per Recommendation #14: You can define custom predicates to control
15 retry behavior beyond the built-in status code and method checks.
16
17 {b Example: Retry on specific error responses}
18 {[
19 let retry_on_rate_limit method_ status headers =
20 status = 429 && Headers.get "x-retry-allowed" headers = Some "true"
21 in
22 let config = Retry.create_config
23 ~retry_response:retry_on_rate_limit
24 ()
25 ]}
26
27 {b Example: Retry on custom exceptions}
28 {[
29 let retry_on_network_error = function
30 | Unix.Unix_error (Unix.ECONNRESET, _, _) -> true
31 | Unix.Unix_error (Unix.ETIMEDOUT, _, _) -> true
32 | _ -> false
33 in
34 let config = Retry.create_config
35 ~retry_exception:retry_on_network_error
36 ()
37 ]}
38*)
39
40open Eio
41
42(** Log source for retry operations *)
43val src : Logs.Src.t
44
45(** {1 Custom Retry Predicates}
46
47 Per Recommendation #14: Allow user-defined retry logic. *)
48
49(** Custom retry predicate for responses.
50 Receives (method, status, headers) and returns true to retry.
51 This runs in addition to the built-in status_forcelist check. *)
52type response_predicate = Method.t -> int -> Headers.t -> bool
53
54(** Custom retry predicate for exceptions.
55 Returns true if the exception should trigger a retry. *)
56type exception_predicate = exn -> bool
57
58(** {1 Configuration} *)
59
60(** Retry configuration *)
61type config = {
62 max_retries : int; (** Maximum number of retry attempts *)
63 backoff_factor : float; (** Exponential backoff multiplier *)
64 backoff_max : float; (** Maximum backoff time in seconds *)
65 status_forcelist : int list; (** HTTP status codes to retry *)
66 allowed_methods : Method.t list; (** Methods safe to retry *)
67 respect_retry_after : bool; (** Honor Retry-After response header *)
68 jitter : bool; (** Add randomness to prevent thundering herd *)
69 retry_response : response_predicate option; (** Custom response retry predicate *)
70 retry_exception : exception_predicate option; (** Custom exception retry predicate *)
71 strict_method_semantics : bool;
72 (** When true, raise an error if asked to retry a non-idempotent method.
73 Per RFC 9110 Section 9.2.2: Non-idempotent methods should not be retried
74 automatically as the request may have already been processed. Default is
75 false (just log and skip retry). *)
76}
77
78(** Default retry configuration *)
79val default_config : config
80
81(** Create a custom retry configuration.
82 @param retry_response Custom predicate for response-based retry decisions
83 @param retry_exception Custom predicate for exception-based retry decisions
84 @param strict_method_semantics When true, raise error on non-idempotent retry *)
85val create_config :
86 ?max_retries:int ->
87 ?backoff_factor:float ->
88 ?backoff_max:float ->
89 ?status_forcelist:int list ->
90 ?allowed_methods:Method.t list ->
91 ?respect_retry_after:bool ->
92 ?jitter:bool ->
93 ?retry_response:response_predicate ->
94 ?retry_exception:exception_predicate ->
95 ?strict_method_semantics:bool ->
96 unit -> config
97
98(** {1 Retry Decision Functions} *)
99
100(** Check if a request should be retried based on built-in rules only.
101 For full custom predicate support, use [should_retry_response]. *)
102val should_retry : config:config -> method_:Method.t -> status:int -> bool
103
104(** Check if a response should be retried, including custom predicates.
105 Returns true if either built-in rules or custom predicate says to retry. *)
106val should_retry_response : config:config -> method_:Method.t -> status:int -> headers:Headers.t -> bool
107
108(** Check if an exception should trigger a retry using custom predicates. *)
109val should_retry_exn : config:config -> exn -> bool
110
111(** Calculate backoff delay for a given attempt *)
112val calculate_backoff : config:config -> attempt:int -> float
113
114(** Parse Retry-After header value (seconds or HTTP date).
115
116 Per {{:https://datatracker.ietf.org/doc/html/rfc9110#section-10.2.3}RFC 9110 Section 10.2.3},
117 Retry-After can be either:
118 - A non-negative integer (delay in seconds)
119 - An HTTP-date (absolute time to retry after)
120
121 Values are capped to [backoff_max] (default 120s) to prevent DoS
122 from malicious servers specifying extremely long delays. *)
123val parse_retry_after : ?backoff_max:float -> string -> float option
124
125(** Execute a request with retry logic *)
126val with_retry :
127 sw:Switch.t ->
128 clock:_ Time.clock ->
129 config:config ->
130 f:(unit -> 'a) ->
131 should_retry_exn:(exn -> bool) ->
132 'a
133
134(** Pretty print retry configuration *)
135val pp_config : Format.formatter -> config -> unit
136
137(** Log retry attempt information *)
138val log_retry : attempt:int -> delay:float -> reason:string -> unit