Skip to main content

rskit_httpclient/
config.rs

1//! HTTP client configuration.
2
3use crate::auth::Auth;
4use crate::destination::DestinationPolicy;
5use rskit_resilience::Policy;
6use rskit_security::TlsConfig;
7use std::collections::HashMap;
8use std::time::Duration;
9
10const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
11const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
12const DEFAULT_MAX_RESPONSE_BODY_BYTES: usize = 10 * 1024 * 1024;
13
14/// Configuration for the HTTP client.
15#[derive(Clone)]
16#[non_exhaustive]
17pub struct HttpClientConfig {
18    /// Base URL for requests (e.g., `https://api.example.com/v1`). Paths are appended to this URL.
19    pub base_url: Option<String>,
20
21    /// Request timeout. Defaults to 30 seconds.
22    pub timeout: Duration,
23
24    /// Connection timeout. Defaults to 10 seconds.
25    pub connect_timeout: Duration,
26
27    /// User-Agent header value. If None, no User-Agent header is set.
28    pub user_agent: Option<String>,
29
30    /// Default headers applied to all requests.
31    pub default_headers: HashMap<String, String>,
32
33    /// Default authentication applied to all requests.
34    pub auth: Option<Auth>,
35
36    /// Follow redirects. Defaults to true.
37    pub follow_redirects: bool,
38
39    /// Maximum number of redirects to follow. Defaults to 5.
40    pub max_redirects: usize,
41
42    /// Maximum response body size accepted by [`HttpClient`](crate::HttpClient).
43    pub max_response_body_bytes: usize,
44
45    /// Destination policy for initial request URLs and redirect targets.
46    pub destination_policy: DestinationPolicy,
47
48    /// Optional resilience policy applied to transport execution.
49    pub resilience_policy: Option<Policy>,
50
51    /// Explicit TLS certificate, trust, and verification configuration.
52    pub tls: Option<TlsConfig>,
53}
54
55impl std::fmt::Debug for HttpClientConfig {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        f.debug_struct("HttpClientConfig")
58            .field("base_url", &self.base_url)
59            .field("timeout", &self.timeout)
60            .field("connect_timeout", &self.connect_timeout)
61            .field("user_agent", &self.user_agent)
62            .field("default_headers", &self.default_headers)
63            .field("auth", &self.auth)
64            .field("follow_redirects", &self.follow_redirects)
65            .field("max_redirects", &self.max_redirects)
66            .field("max_response_body_bytes", &self.max_response_body_bytes)
67            .field("destination_policy", &self.destination_policy)
68            .field("has_resilience_policy", &self.resilience_policy.is_some())
69            .field("tls", &self.tls)
70            .finish()
71    }
72}
73
74impl HttpClientConfig {
75    /// Creates a new HTTP client config with defaults.
76    #[must_use]
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// Sets the base URL.
82    #[must_use]
83    pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
84        self.base_url = Some(url.into());
85        self
86    }
87
88    /// Sets the timeout.
89    #[must_use]
90    pub fn with_timeout(mut self, timeout: Duration) -> Self {
91        self.timeout = timeout;
92        self
93    }
94
95    /// Sets the connection timeout.
96    #[must_use]
97    pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
98        self.connect_timeout = timeout;
99        self
100    }
101
102    /// Sets the User-Agent header.
103    #[must_use]
104    pub fn with_user_agent(mut self, ua: impl Into<String>) -> Self {
105        self.user_agent = Some(ua.into());
106        self
107    }
108
109    /// Adds a default header.
110    #[must_use]
111    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
112        self.default_headers.insert(name.into(), value.into());
113        self
114    }
115
116    /// Sets default headers.
117    #[must_use]
118    pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
119        self.default_headers = headers;
120        self
121    }
122
123    /// Sets default authentication.
124    #[must_use]
125    pub fn with_auth(mut self, auth: Auth) -> Self {
126        self.auth = Some(auth);
127        self
128    }
129
130    /// Sets whether to follow redirects.
131    #[must_use]
132    pub fn with_follow_redirects(mut self, follow: bool) -> Self {
133        self.follow_redirects = follow;
134        self
135    }
136
137    /// Sets the maximum number of redirects to follow.
138    #[must_use]
139    pub fn with_max_redirects(mut self, max: usize) -> Self {
140        self.max_redirects = max;
141        self
142    }
143
144    /// Sets the maximum accepted response body size.
145    #[must_use]
146    pub fn with_max_response_body_bytes(mut self, max: usize) -> Self {
147        self.max_response_body_bytes = max;
148        self
149    }
150
151    /// Sets the destination validation policy.
152    #[must_use]
153    pub fn with_destination_policy(mut self, policy: DestinationPolicy) -> Self {
154        self.destination_policy = policy;
155        self
156    }
157
158    /// Sets the transport resilience policy.
159    #[must_use]
160    pub fn with_resilience_policy(mut self, policy: Policy) -> Self {
161        self.resilience_policy = Some(policy);
162        self
163    }
164
165    /// Sets explicit TLS certificate, trust, and verification configuration.
166    #[must_use]
167    pub fn with_tls(mut self, tls: TlsConfig) -> Self {
168        self.tls = Some(tls);
169        self
170    }
171}
172
173impl Default for HttpClientConfig {
174    fn default() -> Self {
175        Self {
176            base_url: None,
177            timeout: DEFAULT_TIMEOUT,
178            connect_timeout: DEFAULT_CONNECT_TIMEOUT,
179            user_agent: None,
180            default_headers: HashMap::new(),
181            auth: None,
182            follow_redirects: true,
183            max_redirects: 5,
184            max_response_body_bytes: DEFAULT_MAX_RESPONSE_BODY_BYTES,
185            destination_policy: DestinationPolicy::default(),
186            resilience_policy: None,
187            tls: None,
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn debug_redacts_auth_secret_values() {
198        let config = HttpClientConfig::new().with_auth(Auth::bearer("secret-token"));
199
200        let formatted = format!("{config:?}");
201
202        assert!(formatted.contains("SecretString(***)"));
203        assert!(!formatted.contains("secret-token"));
204    }
205
206    #[test]
207    fn builder_methods_override_defaults() {
208        let headers = HashMap::from([("x-default".to_string(), "yes".to_string())]);
209        let policy = DestinationPolicy::new()
210            .with_allowed_schemes(["https"])
211            .with_block_metadata(false);
212        let config = HttpClientConfig::new()
213            .with_timeout(Duration::from_secs(5))
214            .with_connect_timeout(Duration::from_secs(2))
215            .with_headers(headers.clone())
216            .with_follow_redirects(false)
217            .with_max_redirects(0)
218            .with_destination_policy(policy.clone());
219
220        assert_eq!(config.timeout, Duration::from_secs(5));
221        assert_eq!(config.connect_timeout, Duration::from_secs(2));
222        assert_eq!(config.default_headers, headers);
223        assert!(!config.follow_redirects);
224        assert_eq!(config.max_redirects, 0);
225        assert_eq!(config.destination_policy, policy);
226    }
227}