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