1use std::str::FromStr;
12use std::time::Duration;
13
14use http::StatusCode;
15use qubit_config::{ConfigReader, ConfigResult};
16use qubit_retry::{RetryDelay, RetryJitter, RetryOptions};
17
18use super::http_retry_method_policy::HttpRetryMethodPolicy;
19use super::HttpConfigError;
20use crate::{HttpError, HttpErrorKind, HttpRequest, HttpResult};
21
22const DEFAULT_RETRY_MAX_ATTEMPTS: u32 = 3;
23const DEFAULT_RETRY_INITIAL_DELAY: Duration = Duration::from_millis(200);
24const DEFAULT_RETRY_MAX_DELAY: Duration = Duration::from_secs(5);
25const DEFAULT_RETRY_MULTIPLIER: f64 = 2.0;
26const DEFAULT_RETRY_JITTER_FACTOR: f64 = 0.1;
27
28#[derive(Debug, Clone, PartialEq)]
30pub struct HttpRetryOptions {
31 pub enabled: bool,
33 pub max_attempts: u32,
35 pub max_duration: Option<Duration>,
37 pub delay_strategy: RetryDelay,
39 pub jitter_factor: f64,
41 pub method_policy: HttpRetryMethodPolicy,
43 pub retry_status_codes: Option<Vec<StatusCode>>,
47 pub retry_error_kinds: Option<Vec<HttpErrorKind>>,
51}
52
53fn is_retryable_status(status: StatusCode, retry_status_codes: Option<&[StatusCode]>) -> bool {
57 if let Some(status_codes) = retry_status_codes {
58 status_codes.contains(&status)
59 } else {
60 default_retryable_status(status)
61 }
62}
63
64fn is_retryable_error_kind(
68 kind: HttpErrorKind,
69 retry_error_kinds: Option<&[HttpErrorKind]>,
70) -> bool {
71 if let Some(error_kinds) = retry_error_kinds {
72 error_kinds.contains(&kind)
73 } else {
74 default_retryable_error_kind(kind)
75 }
76}
77
78impl HttpRetryOptions {
79 pub fn new() -> Self {
84 Self::default()
85 }
86
87 pub fn from_config<R>(config: &R) -> Result<Self, HttpConfigError>
95 where
96 R: ConfigReader + ?Sized,
97 {
98 let raw = Self::read_config(config).map_err(HttpConfigError::from)?;
99 let mut opts = Self::default();
100
101 if let Some(enabled) = raw.enabled {
102 opts.enabled = enabled;
103 }
104 if let Some(max_attempts) = raw.max_attempts {
105 opts.max_attempts = max_attempts;
106 }
107 opts.max_duration = raw.max_duration;
108 if let Some(jitter_factor) = raw.jitter_factor {
109 opts.jitter_factor = jitter_factor;
110 }
111 if let Some(method_policy) = raw.method_policy.as_ref() {
112 opts.method_policy = HttpRetryMethodPolicy::from_config_value(method_policy)?;
113 }
114 if let Some(status_codes) = raw.status_codes.as_ref() {
115 opts.retry_status_codes = Some(parse_retry_status_codes(status_codes)?);
116 }
117 if let Some(error_kinds) = raw.error_kinds.as_ref() {
118 opts.retry_error_kinds = Some(parse_retry_error_kinds(error_kinds)?);
119 }
120
121 if let Some(delay_strategy) = raw.delay_strategy.as_ref() {
122 opts.delay_strategy = parse_retry_delay_strategy(delay_strategy, &raw)?;
123 }
124
125 opts.validate()?;
126 Ok(opts)
127 }
128
129 fn read_config<R>(config: &R) -> ConfigResult<HttpRetryConfigInput>
138 where
139 R: ConfigReader + ?Sized,
140 {
141 Ok(HttpRetryConfigInput {
142 enabled: config.get_optional("enabled")?,
143 max_attempts: config.get_optional("max_attempts")?,
144 max_duration: config.get_optional("max_duration")?,
145 delay_strategy: config.get_optional_string("delay_strategy")?,
146 fixed_delay: config.get_optional("fixed_delay")?,
147 random_min_delay: config.get_optional("random_min_delay")?,
148 random_max_delay: config.get_optional("random_max_delay")?,
149 backoff_initial_delay: config.get_optional("backoff_initial_delay")?,
150 backoff_max_delay: config.get_optional("backoff_max_delay")?,
151 backoff_multiplier: config.get_optional("backoff_multiplier")?,
152 jitter_factor: config.get_optional("jitter_factor")?,
153 method_policy: config.get_optional_string("method_policy")?,
154 status_codes: config.get_optional_string_list("status_codes")?,
155 error_kinds: config.get_optional_string_list("error_kinds")?,
156 })
157 }
158
159 pub fn validate(&self) -> Result<(), HttpConfigError> {
164 if self.max_attempts == 0 {
165 return Err(HttpConfigError::invalid_value(
166 "max_attempts",
167 "Retry max_attempts must be greater than 0",
168 ));
169 }
170 if !(0.0..=1.0).contains(&self.jitter_factor) {
171 return Err(HttpConfigError::invalid_value(
172 "jitter_factor",
173 "Retry jitter_factor must be between 0.0 and 1.0",
174 ));
175 }
176 self.delay_strategy
177 .validate()
178 .map_err(|message| HttpConfigError::invalid_value("delay_strategy", message))?;
179 Ok(())
180 }
181
182 pub fn allows_method(&self, method: &http::Method) -> bool {
190 self.enabled && self.method_policy.allows_method(method)
191 }
192
193 pub fn should_retry(&self, request: &HttpRequest) -> bool {
202 self.max_attempts > 1 && self.allows_method(request.method())
203 }
204
205 pub fn resolve(&self, request: &HttpRequest) -> Self {
213 let mut options = self.clone();
214 options.enabled = request.retry_override().resolve_enabled(options.enabled);
215 options.method_policy = request
216 .retry_override()
217 .resolve_method_policy(options.method_policy);
218 options
219 }
220
221 pub fn is_retryable_status(&self, status: StatusCode) -> bool {
229 is_retryable_status(status, self.retry_status_codes.as_deref())
230 }
231
232 pub fn is_retryable_error_kind(&self, kind: HttpErrorKind) -> bool {
241 is_retryable_error_kind(kind, self.retry_error_kinds.as_deref())
242 }
243
244 pub(crate) fn to_executor_options(&self) -> HttpResult<RetryOptions> {
254 RetryOptions::new(
255 self.max_attempts,
256 None,
257 self.max_duration,
258 self.delay_strategy.clone(),
259 RetryJitter::factor(self.jitter_factor),
260 )
261 .map_err(|error| HttpError::other(format!("Invalid HTTP retry options: {error}")))
262 }
263}
264
265impl Default for HttpRetryOptions {
266 fn default() -> Self {
267 Self {
268 enabled: false,
269 max_attempts: DEFAULT_RETRY_MAX_ATTEMPTS,
270 max_duration: None,
271 delay_strategy: RetryDelay::Exponential {
272 initial: DEFAULT_RETRY_INITIAL_DELAY,
273 max: DEFAULT_RETRY_MAX_DELAY,
274 multiplier: DEFAULT_RETRY_MULTIPLIER,
275 },
276 jitter_factor: DEFAULT_RETRY_JITTER_FACTOR,
277 method_policy: HttpRetryMethodPolicy::default(),
278 retry_status_codes: None,
279 retry_error_kinds: None,
280 }
281 }
282}
283
284struct HttpRetryConfigInput {
285 enabled: Option<bool>,
286 max_attempts: Option<u32>,
287 max_duration: Option<Duration>,
288 delay_strategy: Option<String>,
289 fixed_delay: Option<Duration>,
290 random_min_delay: Option<Duration>,
291 random_max_delay: Option<Duration>,
292 backoff_initial_delay: Option<Duration>,
293 backoff_max_delay: Option<Duration>,
294 backoff_multiplier: Option<f64>,
295 jitter_factor: Option<f64>,
296 method_policy: Option<String>,
297 status_codes: Option<Vec<String>>,
298 error_kinds: Option<Vec<String>>,
299}
300
301fn parse_retry_delay_strategy(
302 value: &str,
303 raw: &HttpRetryConfigInput,
304) -> Result<RetryDelay, HttpConfigError> {
305 let normalized = value.trim().to_ascii_lowercase().replace('-', "_");
306 match normalized.as_str() {
307 "none" => Ok(RetryDelay::None),
308 "fixed" => Ok(RetryDelay::Fixed(
309 raw.fixed_delay.unwrap_or(DEFAULT_RETRY_INITIAL_DELAY),
310 )),
311 "random" => Ok(RetryDelay::Random {
312 min: raw.random_min_delay.unwrap_or(DEFAULT_RETRY_INITIAL_DELAY),
313 max: raw.random_max_delay.unwrap_or(DEFAULT_RETRY_MAX_DELAY),
314 }),
315 "exponential_backoff" | "exponential" => Ok(RetryDelay::Exponential {
316 initial: raw
317 .backoff_initial_delay
318 .unwrap_or(DEFAULT_RETRY_INITIAL_DELAY),
319 max: raw.backoff_max_delay.unwrap_or(DEFAULT_RETRY_MAX_DELAY),
320 multiplier: raw.backoff_multiplier.unwrap_or(DEFAULT_RETRY_MULTIPLIER),
321 }),
322 _ => Err(HttpConfigError::invalid_value(
323 "delay_strategy",
324 format!("Unsupported retry delay strategy: {value}"),
325 )),
326 }
327}
328
329fn parse_retry_status_codes(values: &[String]) -> Result<Vec<StatusCode>, HttpConfigError> {
341 let mut result = Vec::<StatusCode>::new();
342 for value in values {
343 let trimmed = value.trim();
344 if trimmed.is_empty() {
345 return Err(HttpConfigError::invalid_value(
346 "status_codes",
347 "Retry status_codes cannot contain blank values",
348 ));
349 }
350 let raw_code = trimmed.parse::<u16>().map_err(|error| {
351 HttpConfigError::invalid_value(
352 "status_codes",
353 format!("Invalid retry status code '{trimmed}': {error}"),
354 )
355 })?;
356 if !(100..=599).contains(&raw_code) {
357 return Err(HttpConfigError::invalid_value(
358 "status_codes",
359 format!("Retry status code must be in range 100..=599, got {raw_code}"),
360 ));
361 }
362 let status = StatusCode::from_u16(raw_code)
363 .expect("retry status code range is pre-validated to 100..=599");
364 if !result.contains(&status) {
365 result.push(status);
366 }
367 }
368 result.sort_by_key(|status| status.as_u16());
369 Ok(result)
370}
371
372fn parse_retry_error_kinds(values: &[String]) -> Result<Vec<HttpErrorKind>, HttpConfigError> {
383 let mut result = Vec::<HttpErrorKind>::new();
384 for value in values {
385 let trimmed = value.trim();
386 if trimmed.is_empty() {
387 return Err(HttpConfigError::invalid_value(
388 "error_kinds",
389 "Retry error_kinds cannot contain blank values",
390 ));
391 }
392 let normalized = trimmed.replace('-', "_");
393 let kind = HttpErrorKind::from_str(&normalized).map_err(|_| {
394 HttpConfigError::invalid_value(
395 "error_kinds",
396 format!("Unsupported retry error kind: {trimmed}"),
397 )
398 })?;
399 if !result.contains(&kind) {
400 result.push(kind);
401 }
402 }
403 Ok(result)
404}
405
406fn default_retryable_status(status: StatusCode) -> bool {
415 status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()
416}
417
418fn default_retryable_error_kind(kind: HttpErrorKind) -> bool {
427 matches!(
428 kind,
429 HttpErrorKind::ConnectTimeout
430 | HttpErrorKind::ReadTimeout
431 | HttpErrorKind::WriteTimeout
432 | HttpErrorKind::RequestTimeout
433 | HttpErrorKind::Transport
434 )
435}
436
437#[cfg(coverage)]
442#[doc(hidden)]
443pub(crate) fn coverage_exercise_retry_option_paths() -> String {
444 let options = HttpRetryOptions {
445 jitter_factor: 2.0,
446 ..HttpRetryOptions::default()
447 };
448 options
449 .to_executor_options()
450 .expect_err("invalid jitter should fail executor option conversion")
451 .message
452}