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 std::str::FromStr;
22use url::Url;
23
24use super::from_config_helpers::hashmap_to_headermap;
25use super::http_logging_options::HttpLoggingOptions;
26use super::http_retry_options::HttpRetryOptions;
27use super::http_timeout_options::HttpTimeoutOptions;
28use super::proxy_options::ProxyOptions;
29use super::HttpConfigError;
30use crate::{
31 constants::{
32 DEFAULT_ERROR_RESPONSE_PREVIEW_LIMIT_BYTES,
33 DEFAULT_SSE_MAX_FRAME_BYTES,
34 DEFAULT_SSE_MAX_LINE_BYTES,
35 },
36 request::parse_header,
37 sanitize::{
38 LogSanitizePolicy,
39 LogSanitizer,
40 },
41 sse::{
42 DoneMarkerPolicy,
43 SseJsonMode,
44 },
45 HttpResult,
46};
47
48#[derive(Clone)]
50pub struct HttpClientOptions {
51 pub base_url: Option<Url>,
53 pub default_headers: HeaderMap,
55 pub timeouts: HttpTimeoutOptions,
57 pub proxy: ProxyOptions,
59 pub logging: HttpLoggingOptions,
61 pub error_response_preview_limit: usize,
63 pub user_agent: Option<String>,
65 pub max_redirects: Option<usize>,
67 pub pool_idle_timeout: Option<Duration>,
69 pub pool_max_idle_per_host: Option<usize>,
71 pub use_env_proxy: bool,
74 pub retry: HttpRetryOptions,
76 pub log_sanitize_policy: LogSanitizePolicy,
78 pub ipv4_only: bool,
80 pub sse_json_mode: SseJsonMode,
82 pub sse_done_marker_policy: DoneMarkerPolicy,
84 pub sse_max_line_bytes: usize,
86 pub sse_max_frame_bytes: usize,
88}
89
90impl Default for HttpClientOptions {
91 fn default() -> Self {
98 Self {
99 base_url: None,
100 default_headers: HeaderMap::new(),
101 timeouts: HttpTimeoutOptions::default(),
102 proxy: ProxyOptions::default(),
103 logging: HttpLoggingOptions::default(),
104 error_response_preview_limit: DEFAULT_ERROR_RESPONSE_PREVIEW_LIMIT_BYTES,
105 user_agent: None,
106 max_redirects: None,
107 pool_idle_timeout: None,
108 pool_max_idle_per_host: None,
109 use_env_proxy: false,
110 retry: HttpRetryOptions::default(),
111 log_sanitize_policy: LogSanitizePolicy::default(),
112 ipv4_only: false,
113 sse_json_mode: SseJsonMode::Lenient,
114 sse_done_marker_policy: DoneMarkerPolicy::default(),
115 sse_max_line_bytes: DEFAULT_SSE_MAX_LINE_BYTES,
116 sse_max_frame_bytes: DEFAULT_SSE_MAX_FRAME_BYTES,
117 }
118 }
119}
120
121impl fmt::Debug for HttpClientOptions {
122 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123 let sanitizer = LogSanitizer::for_debug(&self.log_sanitize_policy);
124 let base_url = self
125 .base_url
126 .as_ref()
127 .map(|url| sanitizer.sanitize_url(url));
128 formatter
129 .debug_struct("HttpClientOptions")
130 .field("base_url", &base_url)
131 .field(
132 "default_headers",
133 &sanitizer.sanitize_header_map(&self.default_headers),
134 )
135 .field("timeouts", &self.timeouts)
136 .field("proxy", &self.proxy)
137 .field("logging", &self.logging)
138 .field(
139 "error_response_preview_limit",
140 &self.error_response_preview_limit,
141 )
142 .field("user_agent", &self.user_agent)
143 .field("max_redirects", &self.max_redirects)
144 .field("pool_idle_timeout", &self.pool_idle_timeout)
145 .field("pool_max_idle_per_host", &self.pool_max_idle_per_host)
146 .field("use_env_proxy", &self.use_env_proxy)
147 .field("retry", &self.retry)
148 .field("log_sanitize_policy", &self.log_sanitize_policy)
149 .field("ipv4_only", &self.ipv4_only)
150 .field("sse_json_mode", &self.sse_json_mode)
151 .field("sse_done_marker_policy", &self.sse_done_marker_policy)
152 .field("sse_max_line_bytes", &self.sse_max_line_bytes)
153 .field("sse_max_frame_bytes", &self.sse_max_frame_bytes)
154 .finish()
155 }
156}
157
158struct HttpClientRootConfigInput {
160 base_url: Option<String>,
161 ipv4_only: Option<bool>,
162 error_response_preview_limit: Option<usize>,
163 user_agent: Option<String>,
164 max_redirects: Option<usize>,
165 pool_idle_timeout: Option<Duration>,
166 pool_max_idle_per_host: Option<usize>,
167 use_env_proxy: Option<bool>,
168}
169
170struct HttpClientSseConfigInput {
172 json_mode: Option<String>,
173 done_marker: Option<String>,
174 max_line_bytes: Option<usize>,
175 max_frame_bytes: Option<usize>,
176}
177
178struct HttpClientLogSanitizeConfigInput {
180 sensitive_headers: Option<Vec<String>>,
181 sensitive_query_params: Option<Vec<String>>,
182 sensitive_body_fields: Option<Vec<String>>,
183}
184
185impl HttpClientOptions {
186 fn resolve_config_error<R>(config: &R, mut error: HttpConfigError) -> HttpConfigError
187 where
188 R: ConfigReader + ?Sized,
189 {
190 error.path = if error.path.is_empty() {
191 config.resolve_key("")
192 } else {
193 config.resolve_key(&error.path)
194 };
195 error
196 }
197
198 fn read_config<R>(config: &R) -> ConfigResult<HttpClientRootConfigInput>
199 where
200 R: ConfigReader + ?Sized,
201 {
202 Ok(HttpClientRootConfigInput {
203 base_url: config.get_optional_string("base_url")?,
204 ipv4_only: config.get_optional("ipv4_only")?,
205 error_response_preview_limit: config.get_optional("error_response_preview_limit")?,
206 user_agent: config.get_optional_string("user_agent")?,
207 max_redirects: config.get_optional("max_redirects")?,
208 pool_idle_timeout: config.get_optional("pool_idle_timeout")?,
209 pool_max_idle_per_host: config.get_optional("pool_max_idle_per_host")?,
210 use_env_proxy: config.get_optional("use_env_proxy")?,
211 })
212 }
213
214 fn read_sse_config<R>(config: &R) -> ConfigResult<HttpClientSseConfigInput>
215 where
216 R: ConfigReader + ?Sized,
217 {
218 Ok(HttpClientSseConfigInput {
219 json_mode: config.get_optional_string("json_mode")?,
220 done_marker: config.get_optional_string("done_marker")?,
221 max_line_bytes: config.get_optional("max_line_bytes")?,
222 max_frame_bytes: config.get_optional("max_frame_bytes")?,
223 })
224 }
225
226 fn read_log_sanitize_config<R>(config: &R) -> ConfigResult<HttpClientLogSanitizeConfigInput>
227 where
228 R: ConfigReader + ?Sized,
229 {
230 Ok(HttpClientLogSanitizeConfigInput {
231 sensitive_headers: config.get_optional_string_list("sensitive_headers")?,
232 sensitive_query_params: config.get_optional_string_list("sensitive_query_params")?,
233 sensitive_body_fields: config.get_optional_string_list("sensitive_body_fields")?,
234 })
235 }
236
237 fn parse_sse_done_marker_policy(value: &str) -> Result<DoneMarkerPolicy, HttpConfigError> {
238 let trimmed = value.trim();
239 if trimmed.is_empty() {
240 return Err(HttpConfigError::invalid_value(
241 "done_marker",
242 "Value must not be empty",
243 ));
244 }
245 Ok(DoneMarkerPolicy::from_str(trimmed)
246 .expect("DoneMarkerPolicy::from_str accepts arbitrary custom markers"))
247 }
248
249 fn parse_base_url(base_url: &str) -> Result<Url, HttpConfigError> {
250 Url::parse(base_url).map_err(|error| {
251 HttpConfigError::invalid_value("base_url", format!("Invalid URL: {error}"))
252 })
253 }
254
255 fn parse_sse_json_mode(value: &str) -> Result<SseJsonMode, HttpConfigError> {
256 SseJsonMode::from_str(value.trim()).map_err(|_| {
257 HttpConfigError::invalid_value(
258 "json_mode",
259 format!("Unsupported SSE JSON mode: {value}"),
260 )
261 })
262 }
263
264 fn validate_positive_limit(path: &str, value: usize) -> Result<usize, HttpConfigError> {
265 if value == 0 {
266 return Err(HttpConfigError::invalid_value(
267 path,
268 "Value must be greater than 0",
269 ));
270 }
271 Ok(value)
272 }
273
274 pub fn new() -> Self {
279 Self::default()
280 }
281
282 pub fn set_base_url(&mut self, base_url: &str) -> Result<&mut Self, HttpConfigError> {
290 let parsed = Self::parse_base_url(base_url)?;
291 self.base_url = Some(parsed);
292 Ok(self)
293 }
294
295 pub fn add_header(&mut self, name: &str, value: &str) -> HttpResult<&mut Self> {
304 let (header_name, header_value) = parse_header(name, value)?;
305 self.default_headers.insert(header_name, header_value);
306 Ok(self)
307 }
308
309 pub fn add_headers(&mut self, headers: &[(&str, &str)]) -> HttpResult<&mut Self> {
319 let mut parsed_headers = HeaderMap::new();
320 for &(name, value) in headers {
321 let (header_name, header_value) = parse_header(name, value)?;
322 parsed_headers.insert(header_name, header_value);
323 }
324 self.default_headers.extend(parsed_headers);
325 Ok(self)
326 }
327
328 pub fn from_config<R>(config: &R) -> Result<Self, HttpConfigError>
337 where
338 R: ConfigReader + ?Sized,
339 {
340 let mut opts = HttpClientOptions::default();
341
342 let root = match Self::read_config(config) {
343 Ok(root) => root,
344 Err(error) => {
345 return Err(Self::resolve_config_error(
346 config,
347 HttpConfigError::from(error),
348 ))
349 }
350 };
351
352 if let Some(s) = root.base_url {
353 if let Err(error) = opts.set_base_url(&s) {
354 return Err(Self::resolve_config_error(config, error));
355 }
356 }
357
358 if let Some(v) = root.ipv4_only {
359 opts.ipv4_only = v;
360 }
361 if let Some(limit) = root.error_response_preview_limit {
362 opts.error_response_preview_limit =
363 match Self::validate_positive_limit("error_response_preview_limit", limit) {
364 Ok(limit) => limit,
365 Err(error) => return Err(Self::resolve_config_error(config, error)),
366 };
367 }
368 if let Some(user_agent) = root.user_agent {
369 opts.user_agent = Some(user_agent.trim().to_string());
370 }
371 if let Some(max_redirects) = root.max_redirects {
372 opts.max_redirects = Some(max_redirects);
373 }
374 if let Some(pool_idle_timeout) = root.pool_idle_timeout {
375 opts.pool_idle_timeout = Some(pool_idle_timeout);
376 }
377 if let Some(pool_max_idle_per_host) = root.pool_max_idle_per_host {
378 opts.pool_max_idle_per_host = Some(pool_max_idle_per_host);
379 }
380 if let Some(use_env_proxy) = root.use_env_proxy {
381 opts.use_env_proxy = use_env_proxy;
382 }
383
384 if config.contains_prefix("timeouts") {
386 let timeouts_config = config.prefix_view("timeouts");
387 opts.timeouts = match HttpTimeoutOptions::from_config(&timeouts_config) {
388 Ok(timeouts) => timeouts,
389 Err(error) => return Err(Self::resolve_config_error(&timeouts_config, error)),
390 };
391 }
392
393 if config.contains_prefix("proxy") {
395 let proxy_config = config.prefix_view("proxy");
396 opts.proxy = match ProxyOptions::from_config(&proxy_config) {
397 Ok(proxy) => proxy,
398 Err(error) => return Err(Self::resolve_config_error(&proxy_config, error)),
399 };
400 }
401
402 if config.contains_prefix("logging") {
404 let logging_config = config.prefix_view("logging");
405 opts.logging = match HttpLoggingOptions::from_config(&logging_config) {
406 Ok(logging) => logging,
407 Err(error) => return Err(Self::resolve_config_error(&logging_config, error)),
408 };
409 }
410
411 if config.contains_prefix("retry") {
412 let retry_config = config.prefix_view("retry");
413 opts.retry = match HttpRetryOptions::from_config(&retry_config) {
414 Ok(retry) => retry,
415 Err(error) => return Err(Self::resolve_config_error(&retry_config, error)),
416 };
417 }
418
419 if config.contains_prefix("sse") {
420 let sse_config = config.prefix_view("sse");
421 let sse = match Self::read_sse_config(&sse_config) {
422 Ok(sse) => sse,
423 Err(error) => {
424 return Err(Self::resolve_config_error(
425 &sse_config,
426 HttpConfigError::from(error),
427 ))
428 }
429 };
430 if let Some(mode) = sse.json_mode.as_deref() {
431 opts.sse_json_mode = match Self::parse_sse_json_mode(mode) {
432 Ok(mode) => mode,
433 Err(error) => return Err(Self::resolve_config_error(&sse_config, error)),
434 };
435 }
436 if let Some(marker) = sse.done_marker.as_deref() {
437 opts.sse_done_marker_policy = match Self::parse_sse_done_marker_policy(marker) {
438 Ok(marker) => marker,
439 Err(error) => return Err(Self::resolve_config_error(&sse_config, error)),
440 };
441 }
442 if let Some(max_line_bytes) = sse.max_line_bytes {
443 opts.sse_max_line_bytes =
444 match Self::validate_positive_limit("max_line_bytes", max_line_bytes) {
445 Ok(limit) => limit,
446 Err(error) => return Err(Self::resolve_config_error(&sse_config, error)),
447 };
448 }
449 if let Some(max_frame_bytes) = sse.max_frame_bytes {
450 opts.sse_max_frame_bytes =
451 match Self::validate_positive_limit("max_frame_bytes", max_frame_bytes) {
452 Ok(limit) => limit,
453 Err(error) => return Err(Self::resolve_config_error(&sse_config, error)),
454 };
455 }
456 }
457
458 if config.contains_prefix("log_sanitize") {
459 let log_sanitize_config = config.prefix_view("log_sanitize");
460 let log_sanitize = match Self::read_log_sanitize_config(&log_sanitize_config) {
461 Ok(log_sanitize) => log_sanitize,
462 Err(error) => {
463 return Err(Self::resolve_config_error(
464 &log_sanitize_config,
465 HttpConfigError::from(error),
466 ))
467 }
468 };
469 if let Some(names) = log_sanitize.sensitive_headers {
470 opts.log_sanitize_policy.sensitive_headers.extend(names);
471 }
472 if let Some(names) = log_sanitize.sensitive_query_params {
473 opts.log_sanitize_policy
474 .sensitive_query_params
475 .extend(names);
476 }
477 if let Some(names) = log_sanitize.sensitive_body_fields {
478 opts.log_sanitize_policy.sensitive_body_fields.extend(names);
479 }
480 }
481
482 let headers_prefix = "default_headers";
484 let full_headers_prefix = "default_headers.";
485 let mut header_map: HashMap<String, String> = HashMap::new();
486 for (k, _) in config.iter_prefix(full_headers_prefix) {
487 let header_name = &k[full_headers_prefix.len()..];
488 let value = match config.get_string(k) {
489 Ok(value) => value,
490 Err(error) => {
491 return Err(HttpConfigError::config_error(
492 config.resolve_key(k),
493 error.to_string(),
494 ))
495 }
496 };
497 header_map.insert(header_name.to_string(), value);
498 }
499 let json_headers = match config.get_optional_string(headers_prefix) {
501 Ok(json_headers) => json_headers,
502 Err(error) => {
503 return Err(Self::resolve_config_error(
504 config,
505 HttpConfigError::from(error),
506 ))
507 }
508 };
509 if !header_map.is_empty() && json_headers.is_some() {
510 return Err(HttpConfigError::invalid_value(
511 config.resolve_key(headers_prefix),
512 "default_headers sub-key form and JSON map form cannot be used at the same time",
513 ));
514 }
515 if let Some(json_str) = json_headers {
516 let parsed: HashMap<String, String> = match serde_json::from_str(&json_str) {
517 Ok(parsed) => parsed,
518 Err(error) => {
519 return Err(HttpConfigError::type_error(
520 config.resolve_key(headers_prefix),
521 format!("Failed to parse default_headers JSON: {error}"),
522 ))
523 }
524 };
525 header_map = parsed;
526 }
527 if !header_map.is_empty() {
528 opts.default_headers =
529 hashmap_to_headermap(&config.resolve_key(headers_prefix), header_map)?;
530 }
531
532 Ok(opts)
533 }
534
535 pub fn validate(&self) -> Result<(), HttpConfigError> {
541 self.timeouts
542 .validate()
543 .map_err(|e| e.prepend_path_prefix("timeouts"))?;
544 self.proxy.validate()?;
545 self.logging.validate()?;
546 self.retry
547 .validate()
548 .map_err(|e| e.prepend_path_prefix("retry"))?;
549 Self::validate_positive_limit(
550 "error_response_preview_limit",
551 self.error_response_preview_limit,
552 )?;
553 if let Some(user_agent) = self.user_agent.as_deref() {
554 if user_agent.trim().is_empty() {
555 return Err(HttpConfigError::invalid_value(
556 "user_agent",
557 "Value cannot be empty",
558 ));
559 }
560 HeaderValue::from_str(user_agent).map_err(|error| {
561 HttpConfigError::invalid_value(
562 "user_agent",
563 format!("Invalid header value: {error}"),
564 )
565 })?;
566 }
567 Self::validate_positive_limit("sse.max_line_bytes", self.sse_max_line_bytes)?;
568 Self::validate_positive_limit("sse.max_frame_bytes", self.sse_max_frame_bytes)?;
569 Ok(())
570 }
571}