use crate::auth::Auth;
use crate::destination::DestinationPolicy;
use rskit_resilience::Policy;
use rskit_security::TlsConfig;
use std::collections::HashMap;
use std::time::Duration;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const DEFAULT_MAX_RESPONSE_BODY_BYTES: usize = 10 * 1024 * 1024;
#[derive(Clone)]
#[non_exhaustive]
pub struct HttpClientConfig {
pub base_url: Option<String>,
pub timeout: Duration,
pub connect_timeout: Duration,
pub user_agent: Option<String>,
pub default_headers: HashMap<String, String>,
pub auth: Option<Auth>,
pub follow_redirects: bool,
pub max_redirects: usize,
pub max_response_body_bytes: usize,
pub destination_policy: DestinationPolicy,
pub resilience_policy: Option<Policy>,
pub tls: Option<TlsConfig>,
}
impl std::fmt::Debug for HttpClientConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HttpClientConfig")
.field("base_url", &self.base_url)
.field("timeout", &self.timeout)
.field("connect_timeout", &self.connect_timeout)
.field("user_agent", &self.user_agent)
.field("default_headers", &self.default_headers)
.field("auth", &self.auth)
.field("follow_redirects", &self.follow_redirects)
.field("max_redirects", &self.max_redirects)
.field("max_response_body_bytes", &self.max_response_body_bytes)
.field("destination_policy", &self.destination_policy)
.field("has_resilience_policy", &self.resilience_policy.is_some())
.field("tls", &self.tls)
.finish()
}
}
impl HttpClientConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = Some(url.into());
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = timeout;
self
}
#[must_use]
pub fn with_user_agent(mut self, ua: impl Into<String>) -> Self {
self.user_agent = Some(ua.into());
self
}
#[must_use]
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.default_headers.insert(name.into(), value.into());
self
}
#[must_use]
pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
self.default_headers = headers;
self
}
#[must_use]
pub fn with_auth(mut self, auth: Auth) -> Self {
self.auth = Some(auth);
self
}
#[must_use]
pub fn with_follow_redirects(mut self, follow: bool) -> Self {
self.follow_redirects = follow;
self
}
#[must_use]
pub fn with_max_redirects(mut self, max: usize) -> Self {
self.max_redirects = max;
self
}
#[must_use]
pub fn with_max_response_body_bytes(mut self, max: usize) -> Self {
self.max_response_body_bytes = max;
self
}
#[must_use]
pub fn with_destination_policy(mut self, policy: DestinationPolicy) -> Self {
self.destination_policy = policy;
self
}
#[must_use]
pub fn with_resilience_policy(mut self, policy: Policy) -> Self {
self.resilience_policy = Some(policy);
self
}
#[must_use]
pub fn with_tls(mut self, tls: TlsConfig) -> Self {
self.tls = Some(tls);
self
}
}
impl Default for HttpClientConfig {
fn default() -> Self {
Self {
base_url: None,
timeout: DEFAULT_TIMEOUT,
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
user_agent: None,
default_headers: HashMap::new(),
auth: None,
follow_redirects: true,
max_redirects: 5,
max_response_body_bytes: DEFAULT_MAX_RESPONSE_BODY_BYTES,
destination_policy: DestinationPolicy::default(),
resilience_policy: None,
tls: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_redacts_auth_secret_values() {
let config = HttpClientConfig::new().with_auth(Auth::bearer("secret-token"));
let formatted = format!("{config:?}");
assert!(formatted.contains("SecretString(***)"));
assert!(!formatted.contains("secret-token"));
}
#[test]
fn builder_methods_override_defaults() {
let headers = HashMap::from([("x-default".to_string(), "yes".to_string())]);
let policy = DestinationPolicy::new()
.with_allowed_schemes(["https"])
.with_block_metadata(false);
let config = HttpClientConfig::new()
.with_timeout(Duration::from_secs(5))
.with_connect_timeout(Duration::from_secs(2))
.with_headers(headers.clone())
.with_follow_redirects(false)
.with_max_redirects(0)
.with_destination_policy(policy.clone());
assert_eq!(config.timeout, Duration::from_secs(5));
assert_eq!(config.connect_timeout, Duration::from_secs(2));
assert_eq!(config.default_headers, headers);
assert!(!config.follow_redirects);
assert_eq!(config.max_redirects, 0);
assert_eq!(config.destination_policy, policy);
}
}