A batteries included HTTP/1.1 client in OCaml
at main 138 lines 5.3 kB view raw
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