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