Skip to main content

qubit_http/options/
http_timeout_options.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10
11use std::time::Duration;
12
13use qubit_config::{
14    ConfigReader,
15    ConfigResult,
16};
17
18use super::HttpConfigError;
19use crate::constants::{
20    DEFAULT_CONNECT_TIMEOUT_SECS,
21    DEFAULT_READ_TIMEOUT_SECS,
22    DEFAULT_WRITE_TIMEOUT_SECS,
23};
24
25/// Connect, read, write, and optional whole-request timeouts for HTTP I/O.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct HttpTimeoutOptions {
28    /// Connect timeout.
29    pub connect_timeout: Duration,
30    /// Read timeout.
31    pub read_timeout: Duration,
32    /// Write timeout.
33    pub write_timeout: Duration,
34    /// Optional global request timeout.
35    pub request_timeout: Option<Duration>,
36}
37
38impl Default for HttpTimeoutOptions {
39    /// Connect / read / write durations use
40    /// [`crate::constants::DEFAULT_CONNECT_TIMEOUT_SECS`],
41    /// [`crate::constants::DEFAULT_READ_TIMEOUT_SECS`], and
42    /// [`crate::constants::DEFAULT_WRITE_TIMEOUT_SECS`]; no global request timeout.
43    ///
44    /// # Returns
45    /// Default [`HttpTimeoutOptions`].
46    fn default() -> Self {
47        Self {
48            connect_timeout: Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_SECS),
49            read_timeout: Duration::from_secs(DEFAULT_READ_TIMEOUT_SECS),
50            write_timeout: Duration::from_secs(DEFAULT_WRITE_TIMEOUT_SECS),
51            request_timeout: None,
52        }
53    }
54}
55
56struct TimeoutConfigInput {
57    connect_timeout: Option<Duration>,
58    read_timeout: Option<Duration>,
59    write_timeout: Option<Duration>,
60    request_timeout: Option<Duration>,
61}
62
63fn read_timeout_config<R>(config: &R) -> ConfigResult<TimeoutConfigInput>
64where
65    R: ConfigReader + ?Sized,
66{
67    Ok(TimeoutConfigInput {
68        connect_timeout: config.get_optional("connect_timeout")?,
69        read_timeout: config.get_optional("read_timeout")?,
70        write_timeout: config.get_optional("write_timeout")?,
71        request_timeout: config.get_optional("request_timeout")?,
72    })
73}
74
75impl HttpTimeoutOptions {
76    /// Validates timeout bounds.
77    ///
78    /// # Returns
79    /// `Ok(())` when all configured durations are strictly greater than zero.
80    pub fn validate(&self) -> Result<(), HttpConfigError> {
81        validate_positive_duration("connect_timeout", self.connect_timeout)?;
82        validate_positive_duration("read_timeout", self.read_timeout)?;
83        validate_positive_duration("write_timeout", self.write_timeout)?;
84        if let Some(request_timeout) = self.request_timeout {
85            validate_positive_duration("request_timeout", request_timeout)?;
86        }
87        Ok(())
88    }
89
90    /// Reads timeout settings from `config` using **relative** keys.
91    ///
92    /// # Parameters
93    /// - `config`: Any [`ConfigReader`] (e.g. root [`qubit_config::Config`] or
94    ///   `config.prefix_view("timeouts")`).
95    ///
96    /// Keys read (all optional; missing keys keep their defaults):
97    /// - `connect_timeout`
98    /// - `read_timeout`
99    /// - `write_timeout`
100    /// - `request_timeout`
101    ///
102    /// # Returns
103    /// Populated [`HttpTimeoutOptions`] or [`HttpConfigError`] on type conversion
104    /// failure.
105    pub fn from_config<R>(config: &R) -> Result<Self, HttpConfigError>
106    where
107        R: ConfigReader + ?Sized,
108    {
109        let raw = read_timeout_config(config).map_err(HttpConfigError::from)?;
110
111        let mut opts = HttpTimeoutOptions::default();
112        if let Some(d) = raw.connect_timeout {
113            opts.connect_timeout = d;
114        }
115        if let Some(d) = raw.read_timeout {
116            opts.read_timeout = d;
117        }
118        if let Some(d) = raw.write_timeout {
119            opts.write_timeout = d;
120        }
121        opts.request_timeout = raw.request_timeout;
122        opts.validate()?;
123        Ok(opts)
124    }
125}
126
127fn validate_positive_duration(path: &str, value: Duration) -> Result<(), HttpConfigError> {
128    if value.is_zero() {
129        return Err(HttpConfigError::invalid_value(
130            path,
131            "Timeout value must be greater than 0",
132        ));
133    }
134    Ok(())
135}