1use std::collections::HashMap;
12use std::fmt;
13use std::time::Duration;
14
15use http::HeaderMap;
16use http::HeaderValue;
17use qubit_config::{
18 ConfigReader,
19 ConfigResult,
20};
21use qubit_sanitize::SensitivityLevel;
22use std::str::FromStr;
23use url::Url;
24
25use super::from_config_helpers::hashmap_to_headermap;
26use super::http_logging_options::HttpLoggingOptions;
27use super::http_retry_options::HttpRetryOptions;
28use super::http_timeout_options::HttpTimeoutOptions;
29use super::proxy_options::ProxyOptions;
30use super::HttpConfigError;
31use crate::{
32 constants::{
33 DEFAULT_ERROR_RESPONSE_PREVIEW_LIMIT_BYTES,
34 DEFAULT_SSE_MAX_FRAME_BYTES,
35 DEFAULT_SSE_MAX_LINE_BYTES,
36 },
37 request::parse_header,
38 sanitize::{
39 LogSanitizePolicy,
40 SanitizedDebugger,
41 },
42 sse::{
43 DoneMarkerPolicy,
44 SseJsonMode,
45 },
46 HttpResult,
47};
48
49#[derive(Clone)]
51pub struct HttpClientOptions {
52 pub base_url: Option<Url>,
54 pub default_headers: HeaderMap,
56 pub timeouts: HttpTimeoutOptions,
58 pub proxy: ProxyOptions,
60 pub logging: HttpLoggingOptions,
62 pub error_response_preview_limit: usize,
64 pub user_agent: Option<String>,
66 pub max_redirects: Option<usize>,
68 pub pool_idle_timeout: Option<Duration>,
70 pub pool_max_idle_per_host: Option<usize>,
72 pub use_env_proxy: bool,
75 pub retry: HttpRetryOptions,
77 pub log_sanitize_policy: LogSanitizePolicy,
79 pub ipv4_only: bool,
81 pub sse_json_mode: SseJsonMode,
83 pub sse_done_marker_policy: DoneMarkerPolicy,
85 pub sse_max_line_bytes: usize,
87 pub sse_max_frame_bytes: usize,
89}
90
91impl Default for HttpClientOptions {
92 fn default() -> Self {
99 Self {
100 base_url: None,
101 default_headers: HeaderMap::new(),
102 timeouts: HttpTimeoutOptions::default(),
103 proxy: ProxyOptions::default(),
104 logging: HttpLoggingOptions::default(),
105 error_response_preview_limit: DEFAULT_ERROR_RESPONSE_PREVIEW_LIMIT_BYTES,
106 user_agent: None,
107 max_redirects: None,
108 pool_idle_timeout: None,
109 pool_max_idle_per_host: None,
110 use_env_proxy: false,
111 retry: HttpRetryOptions::default(),
112 log_sanitize_policy: LogSanitizePolicy::default(),
113 ipv4_only: false,
114 sse_json_mode: SseJsonMode::Lenient,
115 sse_done_marker_policy: DoneMarkerPolicy::default(),
116 sse_max_line_bytes: DEFAULT_SSE_MAX_LINE_BYTES,
117 sse_max_frame_bytes: DEFAULT_SSE_MAX_FRAME_BYTES,
118 }
119 }
120}
121
122impl fmt::Debug for HttpClientOptions {
123 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
124 let debugger = SanitizedDebugger::new(&self.log_sanitize_policy);
125 let base_url = debugger.optional_url(self.base_url.as_ref());
126 formatter
127 .debug_struct("HttpClientOptions")
128 .field("base_url", &base_url)
129 .field("default_headers", &debugger.headers(&self.default_headers))
130 .field("timeouts", &self.timeouts)
131 .field("proxy", &self.proxy)
132 .field("logging", &self.logging)
133 .field("error_response_preview_limit", &self.error_response_preview_limit)
134 .field("user_agent", &self.user_agent)
135 .field("max_redirects", &self.max_redirects)
136 .field("pool_idle_timeout", &self.pool_idle_timeout)
137 .field("pool_max_idle_per_host", &self.pool_max_idle_per_host)
138 .field("use_env_proxy", &self.use_env_proxy)
139 .field("retry", &self.retry)
140 .field("log_sanitize_policy", &self.log_sanitize_policy)
141 .field("ipv4_only", &self.ipv4_only)
142 .field("sse_json_mode", &self.sse_json_mode)
143 .field("sse_done_marker_policy", &self.sse_done_marker_policy)
144 .field("sse_max_line_bytes", &self.sse_max_line_bytes)
145 .field("sse_max_frame_bytes", &self.sse_max_frame_bytes)
146 .finish()
147 }
148}
149
150struct HttpClientRootConfigInput {
152 base_url: Option<String>,
153 ipv4_only: Option<bool>,
154 error_response_preview_limit: Option<usize>,
155 user_agent: Option<String>,
156 max_redirects: Option<usize>,
157 pool_idle_timeout: Option<Duration>,
158 pool_max_idle_per_host: Option<usize>,
159 use_env_proxy: Option<bool>,
160}
161
162struct HttpClientSseConfigInput {
164 json_mode: Option<String>,
165 done_marker: Option<String>,
166 max_line_bytes: Option<usize>,
167 max_frame_bytes: Option<usize>,
168}
169
170struct HttpClientLogSanitizeConfigInput {
172 sensitive_headers: Option<Vec<String>>,
173 sensitive_query_params: Option<Vec<String>>,
174 sensitive_body_fields: Option<Vec<String>>,
175}
176
177impl HttpClientOptions {
178 fn resolve_config_error<R>(config: &R, mut error: HttpConfigError) -> HttpConfigError
179 where
180 R: ConfigReader + ?Sized,
181 {
182 error.path = if error.path.is_empty() {
183 config.resolve_key("")
184 } else {
185 config.resolve_key(&error.path)
186 };
187 error
188 }
189
190 fn read_config<R>(config: &R) -> ConfigResult<HttpClientRootConfigInput>
191 where
192 R: ConfigReader + ?Sized,
193 {
194 Ok(HttpClientRootConfigInput {
195 base_url: config.get_optional_string("base_url")?,
196 ipv4_only: config.get_optional("ipv4_only")?,
197 error_response_preview_limit: config.get_optional("error_response_preview_limit")?,
198 user_agent: config.get_optional_string("user_agent")?,
199 max_redirects: config.get_optional("max_redirects")?,
200 pool_idle_timeout: config.get_optional("pool_idle_timeout")?,
201 pool_max_idle_per_host: config.get_optional("pool_max_idle_per_host")?,
202 use_env_proxy: config.get_optional("use_env_proxy")?,
203 })
204 }
205
206 fn read_sse_config<R>(config: &R) -> ConfigResult<HttpClientSseConfigInput>
207 where
208 R: ConfigReader + ?Sized,
209 {
210 Ok(HttpClientSseConfigInput {
211 json_mode: config.get_optional_string("json_mode")?,
212 done_marker: config.get_optional_string("done_marker")?,
213 max_line_bytes: config.get_optional("max_line_bytes")?,
214 max_frame_bytes: config.get_optional("max_frame_bytes")?,
215 })
216 }
217
218 fn read_log_sanitize_config<R>(config: &R) -> ConfigResult<HttpClientLogSanitizeConfigInput>
219 where
220 R: ConfigReader + ?Sized,
221 {
222 Ok(HttpClientLogSanitizeConfigInput {
223 sensitive_headers: config.get_optional_string_list("sensitive_headers")?,
224 sensitive_query_params: config.get_optional_string_list("sensitive_query_params")?,
225 sensitive_body_fields: config.get_optional_string_list("sensitive_body_fields")?,
226 })
227 }
228
229 fn parse_sse_done_marker_policy(value: &str) -> Result<DoneMarkerPolicy, HttpConfigError> {
230 let trimmed = value.trim();
231 if trimmed.is_empty() {
232 return Err(HttpConfigError::invalid_value("done_marker", "Value must not be empty"));
233 }
234 Ok(DoneMarkerPolicy::from_str(trimmed).expect("DoneMarkerPolicy::from_str accepts arbitrary custom markers"))
235 }
236
237 fn parse_base_url(base_url: &str) -> Result<Url, HttpConfigError> {
238 Url::parse(base_url)
239 .map_err(|error| HttpConfigError::invalid_value("base_url", format!("Invalid URL: {error}")))
240 }
241
242 fn parse_sse_json_mode(value: &str) -> Result<SseJsonMode, HttpConfigError> {
243 SseJsonMode::from_str(value.trim())
244 .map_err(|_| HttpConfigError::invalid_value("json_mode", format!("Unsupported SSE JSON mode: {value}")))
245 }
246
247 fn validate_positive_limit(path: &str, value: usize) -> Result<usize, HttpConfigError> {
248 if value == 0 {
249 return Err(HttpConfigError::invalid_value(path, "Value must be greater than 0"));
250 }
251 Ok(value)
252 }
253
254 pub fn new() -> Self {
259 Self::default()
260 }
261
262 pub fn set_base_url(&mut self, base_url: &str) -> Result<&mut Self, HttpConfigError> {
270 let parsed = Self::parse_base_url(base_url)?;
271 self.base_url = Some(parsed);
272 Ok(self)
273 }
274
275 pub fn add_header(&mut self, name: &str, value: &str) -> HttpResult<&mut Self> {
284 let (header_name, header_value) = parse_header(name, value)?;
285 self.default_headers.insert(header_name, header_value);
286 Ok(self)
287 }
288
289 pub fn add_headers(&mut self, headers: &[(&str, &str)]) -> HttpResult<&mut Self> {
299 let mut parsed_headers = HeaderMap::new();
300 for &(name, value) in headers {
301 let (header_name, header_value) = parse_header(name, value)?;
302 parsed_headers.insert(header_name, header_value);
303 }
304 self.default_headers.extend(parsed_headers);
305 Ok(self)
306 }
307
308 pub fn from_config<R>(config: &R) -> Result<Self, HttpConfigError>
317 where
318 R: ConfigReader + ?Sized,
319 {
320 let mut opts = HttpClientOptions::default();
321
322 let root = match Self::read_config(config) {
323 Ok(root) => root,
324 Err(error) => return Err(Self::resolve_config_error(config, HttpConfigError::from(error))),
325 };
326
327 if let Some(s) = root.base_url {
328 if let Err(error) = opts.set_base_url(&s) {
329 return Err(Self::resolve_config_error(config, error));
330 }
331 }
332
333 if let Some(v) = root.ipv4_only {
334 opts.ipv4_only = v;
335 }
336 if let Some(limit) = root.error_response_preview_limit {
337 opts.error_response_preview_limit =
338 match Self::validate_positive_limit("error_response_preview_limit", limit) {
339 Ok(limit) => limit,
340 Err(error) => return Err(Self::resolve_config_error(config, error)),
341 };
342 }
343 if let Some(user_agent) = root.user_agent {
344 opts.user_agent = Some(user_agent.trim().to_string());
345 }
346 if let Some(max_redirects) = root.max_redirects {
347 opts.max_redirects = Some(max_redirects);
348 }
349 if let Some(pool_idle_timeout) = root.pool_idle_timeout {
350 opts.pool_idle_timeout = Some(pool_idle_timeout);
351 }
352 if let Some(pool_max_idle_per_host) = root.pool_max_idle_per_host {
353 opts.pool_max_idle_per_host = Some(pool_max_idle_per_host);
354 }
355 if let Some(use_env_proxy) = root.use_env_proxy {
356 opts.use_env_proxy = use_env_proxy;
357 }
358
359 if config.contains_prefix("timeouts") {
361 let timeouts_config = config.prefix_view("timeouts");
362 opts.timeouts = match HttpTimeoutOptions::from_config(&timeouts_config) {
363 Ok(timeouts) => timeouts,
364 Err(error) => return Err(Self::resolve_config_error(&timeouts_config, error)),
365 };
366 }
367
368 if config.contains_prefix("proxy") {
370 let proxy_config = config.prefix_view("proxy");
371 opts.proxy = match ProxyOptions::from_config(&proxy_config) {
372 Ok(proxy) => proxy,
373 Err(error) => return Err(Self::resolve_config_error(&proxy_config, error)),
374 };
375 }
376
377 if config.contains_prefix("logging") {
379 let logging_config = config.prefix_view("logging");
380 opts.logging = match HttpLoggingOptions::from_config(&logging_config) {
381 Ok(logging) => logging,
382 Err(error) => return Err(Self::resolve_config_error(&logging_config, error)),
383 };
384 }
385
386 if config.contains_prefix("retry") {
387 let retry_config = config.prefix_view("retry");
388 opts.retry = match HttpRetryOptions::from_config(&retry_config) {
389 Ok(retry) => retry,
390 Err(error) => return Err(Self::resolve_config_error(&retry_config, error)),
391 };
392 }
393
394 if config.contains_prefix("sse") {
395 let sse_config = config.prefix_view("sse");
396 let sse = match Self::read_sse_config(&sse_config) {
397 Ok(sse) => sse,
398 Err(error) => return Err(Self::resolve_config_error(&sse_config, HttpConfigError::from(error))),
399 };
400 if let Some(mode) = sse.json_mode.as_deref() {
401 opts.sse_json_mode = match Self::parse_sse_json_mode(mode) {
402 Ok(mode) => mode,
403 Err(error) => return Err(Self::resolve_config_error(&sse_config, error)),
404 };
405 }
406 if let Some(marker) = sse.done_marker.as_deref() {
407 opts.sse_done_marker_policy = match Self::parse_sse_done_marker_policy(marker) {
408 Ok(marker) => marker,
409 Err(error) => return Err(Self::resolve_config_error(&sse_config, error)),
410 };
411 }
412 if let Some(max_line_bytes) = sse.max_line_bytes {
413 opts.sse_max_line_bytes = match Self::validate_positive_limit("max_line_bytes", max_line_bytes) {
414 Ok(limit) => limit,
415 Err(error) => return Err(Self::resolve_config_error(&sse_config, error)),
416 };
417 }
418 if let Some(max_frame_bytes) = sse.max_frame_bytes {
419 opts.sse_max_frame_bytes = match Self::validate_positive_limit("max_frame_bytes", max_frame_bytes) {
420 Ok(limit) => limit,
421 Err(error) => return Err(Self::resolve_config_error(&sse_config, error)),
422 };
423 }
424 }
425
426 if config.contains_prefix("log_sanitize") {
427 let log_sanitize_config = config.prefix_view("log_sanitize");
428 let log_sanitize = match Self::read_log_sanitize_config(&log_sanitize_config) {
429 Ok(log_sanitize) => log_sanitize,
430 Err(error) => {
431 return Err(Self::resolve_config_error(
432 &log_sanitize_config,
433 HttpConfigError::from(error),
434 ))
435 }
436 };
437 if let Some(names) = log_sanitize.sensitive_headers {
438 opts.log_sanitize_policy
439 .sensitive_headers
440 .extend(names, SensitivityLevel::High);
441 }
442 if let Some(names) = log_sanitize.sensitive_query_params {
443 opts.log_sanitize_policy
444 .sensitive_query_params
445 .extend(names, SensitivityLevel::High);
446 }
447 if let Some(names) = log_sanitize.sensitive_body_fields {
448 opts.log_sanitize_policy
449 .sensitive_body_fields
450 .extend(names, SensitivityLevel::High);
451 }
452 }
453
454 let headers_prefix = "default_headers";
456 let full_headers_prefix = "default_headers.";
457 let mut header_map: HashMap<String, String> = HashMap::new();
458 for (k, _) in config.iter_prefix(full_headers_prefix) {
459 let header_name = &k[full_headers_prefix.len()..];
460 let value = match config.get_string(k) {
461 Ok(value) => value,
462 Err(error) => return Err(HttpConfigError::config_error(config.resolve_key(k), error.to_string())),
463 };
464 header_map.insert(header_name.to_string(), value);
465 }
466 let json_headers = match config.get_optional_string(headers_prefix) {
468 Ok(json_headers) => json_headers,
469 Err(error) => return Err(Self::resolve_config_error(config, HttpConfigError::from(error))),
470 };
471 if !header_map.is_empty() && json_headers.is_some() {
472 return Err(HttpConfigError::invalid_value(
473 config.resolve_key(headers_prefix),
474 "default_headers sub-key form and JSON map form cannot be used at the same time",
475 ));
476 }
477 if let Some(json_str) = json_headers {
478 let parsed: HashMap<String, String> = match serde_json::from_str(&json_str) {
479 Ok(parsed) => parsed,
480 Err(error) => {
481 return Err(HttpConfigError::type_error(
482 config.resolve_key(headers_prefix),
483 format!("Failed to parse default_headers JSON: {error}"),
484 ))
485 }
486 };
487 header_map = parsed;
488 }
489 if !header_map.is_empty() {
490 opts.default_headers = hashmap_to_headermap(&config.resolve_key(headers_prefix), header_map)?;
491 }
492
493 Ok(opts)
494 }
495
496 pub fn validate(&self) -> Result<(), HttpConfigError> {
502 self.timeouts
503 .validate()
504 .map_err(|e| e.prepend_path_prefix("timeouts"))?;
505 self.proxy.validate()?;
506 self.logging.validate()?;
507 self.retry.validate().map_err(|e| e.prepend_path_prefix("retry"))?;
508 Self::validate_positive_limit("error_response_preview_limit", self.error_response_preview_limit)?;
509 if let Some(user_agent) = self.user_agent.as_deref() {
510 if user_agent.trim().is_empty() {
511 return Err(HttpConfigError::invalid_value("user_agent", "Value cannot be empty"));
512 }
513 HeaderValue::from_str(user_agent).map_err(|error| {
514 HttpConfigError::invalid_value("user_agent", format!("Invalid header value: {error}"))
515 })?;
516 }
517 Self::validate_positive_limit("sse.max_line_bytes", self.sse_max_line_bytes)?;
518 Self::validate_positive_limit("sse.max_frame_bytes", self.sse_max_frame_bytes)?;
519 Ok(())
520 }
521}