1use std::collections::HashMap;
12use std::time::Duration;
13
14use http::HeaderMap;
15use http::HeaderValue;
16use qubit_config::{
17 ConfigReader,
18 ConfigResult,
19};
20use std::str::FromStr;
21use url::Url;
22
23use super::from_config_helpers::hashmap_to_headermap;
24use super::http_logging_options::HttpLoggingOptions;
25use super::http_retry_options::HttpRetryOptions;
26use super::http_timeout_options::HttpTimeoutOptions;
27use super::proxy_options::ProxyOptions;
28use super::sensitive_http_headers::SensitiveHttpHeaders;
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 sse::{
38 DoneMarkerPolicy,
39 SseJsonMode,
40 },
41 HttpResult,
42};
43
44#[derive(Debug, Clone)]
46pub struct HttpClientOptions {
47 pub base_url: Option<Url>,
49 pub default_headers: HeaderMap,
51 pub timeouts: HttpTimeoutOptions,
53 pub proxy: ProxyOptions,
55 pub logging: HttpLoggingOptions,
57 pub error_response_preview_limit: usize,
59 pub user_agent: Option<String>,
61 pub max_redirects: Option<usize>,
63 pub pool_idle_timeout: Option<Duration>,
65 pub pool_max_idle_per_host: Option<usize>,
67 pub use_env_proxy: bool,
70 pub retry: HttpRetryOptions,
72 pub sensitive_headers: SensitiveHttpHeaders,
74 pub ipv4_only: bool,
76 pub sse_json_mode: SseJsonMode,
78 pub sse_done_marker_policy: DoneMarkerPolicy,
80 pub sse_max_line_bytes: usize,
82 pub sse_max_frame_bytes: usize,
84}
85
86impl Default for HttpClientOptions {
87 fn default() -> Self {
94 Self {
95 base_url: None,
96 default_headers: HeaderMap::new(),
97 timeouts: HttpTimeoutOptions::default(),
98 proxy: ProxyOptions::default(),
99 logging: HttpLoggingOptions::default(),
100 error_response_preview_limit: DEFAULT_ERROR_RESPONSE_PREVIEW_LIMIT_BYTES,
101 user_agent: None,
102 max_redirects: None,
103 pool_idle_timeout: None,
104 pool_max_idle_per_host: None,
105 use_env_proxy: false,
106 retry: HttpRetryOptions::default(),
107 sensitive_headers: SensitiveHttpHeaders::default(),
108 ipv4_only: false,
109 sse_json_mode: SseJsonMode::Lenient,
110 sse_done_marker_policy: DoneMarkerPolicy::default(),
111 sse_max_line_bytes: DEFAULT_SSE_MAX_LINE_BYTES,
112 sse_max_frame_bytes: DEFAULT_SSE_MAX_FRAME_BYTES,
113 }
114 }
115}
116
117struct HttpClientRootConfigInput {
119 base_url: Option<String>,
120 ipv4_only: Option<bool>,
121 error_response_preview_limit: Option<usize>,
122 user_agent: Option<String>,
123 max_redirects: Option<usize>,
124 pool_idle_timeout: Option<Duration>,
125 pool_max_idle_per_host: Option<usize>,
126 use_env_proxy: Option<bool>,
127 sensitive_headers: Option<Vec<String>>,
128}
129
130struct HttpClientSseConfigInput {
132 json_mode: Option<String>,
133 done_marker: Option<String>,
134 max_line_bytes: Option<usize>,
135 max_frame_bytes: Option<usize>,
136}
137
138impl HttpClientOptions {
139 fn resolve_config_error<R>(config: &R, mut error: HttpConfigError) -> HttpConfigError
140 where
141 R: ConfigReader + ?Sized,
142 {
143 error.path = if error.path.is_empty() {
144 config.resolve_key("")
145 } else {
146 config.resolve_key(&error.path)
147 };
148 error
149 }
150
151 fn read_config<R>(config: &R) -> ConfigResult<HttpClientRootConfigInput>
152 where
153 R: ConfigReader + ?Sized,
154 {
155 Ok(HttpClientRootConfigInput {
156 base_url: config.get_optional_string("base_url")?,
157 ipv4_only: config.get_optional("ipv4_only")?,
158 error_response_preview_limit: config.get_optional("error_response_preview_limit")?,
159 user_agent: config.get_optional_string("user_agent")?,
160 max_redirects: config.get_optional("max_redirects")?,
161 pool_idle_timeout: config.get_optional("pool_idle_timeout")?,
162 pool_max_idle_per_host: config.get_optional("pool_max_idle_per_host")?,
163 use_env_proxy: config.get_optional("use_env_proxy")?,
164 sensitive_headers: config.get_optional_string_list("sensitive_headers")?,
165 })
166 }
167
168 fn read_sse_config<R>(config: &R) -> ConfigResult<HttpClientSseConfigInput>
169 where
170 R: ConfigReader + ?Sized,
171 {
172 Ok(HttpClientSseConfigInput {
173 json_mode: config.get_optional_string("json_mode")?,
174 done_marker: config.get_optional_string("done_marker")?,
175 max_line_bytes: config.get_optional("max_line_bytes")?,
176 max_frame_bytes: config.get_optional("max_frame_bytes")?,
177 })
178 }
179
180 fn parse_sse_done_marker_policy(value: &str) -> Result<DoneMarkerPolicy, HttpConfigError> {
181 let trimmed = value.trim();
182 if trimmed.is_empty() {
183 return Err(HttpConfigError::invalid_value(
184 "done_marker",
185 "Value must not be empty",
186 ));
187 }
188 Ok(DoneMarkerPolicy::from_str(trimmed)
189 .expect("DoneMarkerPolicy::from_str accepts arbitrary custom markers"))
190 }
191
192 fn parse_base_url(base_url: &str) -> Result<Url, HttpConfigError> {
193 Url::parse(base_url).map_err(|error| {
194 HttpConfigError::invalid_value("base_url", format!("Invalid URL: {error}"))
195 })
196 }
197
198 fn parse_sse_json_mode(value: &str) -> Result<SseJsonMode, HttpConfigError> {
199 SseJsonMode::from_str(value.trim()).map_err(|_| {
200 HttpConfigError::invalid_value(
201 "json_mode",
202 format!("Unsupported SSE JSON mode: {value}"),
203 )
204 })
205 }
206
207 fn validate_positive_limit(path: &str, value: usize) -> Result<usize, HttpConfigError> {
208 if value == 0 {
209 return Err(HttpConfigError::invalid_value(
210 path,
211 "Value must be greater than 0",
212 ));
213 }
214 Ok(value)
215 }
216
217 pub fn new() -> Self {
222 Self::default()
223 }
224
225 pub fn set_base_url(&mut self, base_url: &str) -> Result<&mut Self, HttpConfigError> {
233 let parsed = Self::parse_base_url(base_url)?;
234 self.base_url = Some(parsed);
235 Ok(self)
236 }
237
238 pub fn add_header(&mut self, name: &str, value: &str) -> HttpResult<&mut Self> {
247 let (header_name, header_value) = parse_header(name, value)?;
248 self.default_headers.insert(header_name, header_value);
249 Ok(self)
250 }
251
252 pub fn add_headers(&mut self, headers: &[(&str, &str)]) -> HttpResult<&mut Self> {
262 let mut parsed_headers = HeaderMap::new();
263 for &(name, value) in headers {
264 let (header_name, header_value) = parse_header(name, value)?;
265 parsed_headers.insert(header_name, header_value);
266 }
267 self.default_headers.extend(parsed_headers);
268 Ok(self)
269 }
270
271 pub fn from_config<R>(config: &R) -> Result<Self, HttpConfigError>
280 where
281 R: ConfigReader + ?Sized,
282 {
283 let mut opts = HttpClientOptions::default();
284
285 let root = match Self::read_config(config) {
286 Ok(root) => root,
287 Err(error) => {
288 return Err(Self::resolve_config_error(
289 config,
290 HttpConfigError::from(error),
291 ))
292 }
293 };
294
295 if let Some(s) = root.base_url {
296 if let Err(error) = opts.set_base_url(&s) {
297 return Err(Self::resolve_config_error(config, error));
298 }
299 }
300
301 if let Some(v) = root.ipv4_only {
302 opts.ipv4_only = v;
303 }
304 if let Some(limit) = root.error_response_preview_limit {
305 opts.error_response_preview_limit =
306 match Self::validate_positive_limit("error_response_preview_limit", limit) {
307 Ok(limit) => limit,
308 Err(error) => return Err(Self::resolve_config_error(config, error)),
309 };
310 }
311 if let Some(user_agent) = root.user_agent {
312 opts.user_agent = Some(user_agent.trim().to_string());
313 }
314 if let Some(max_redirects) = root.max_redirects {
315 opts.max_redirects = Some(max_redirects);
316 }
317 if let Some(pool_idle_timeout) = root.pool_idle_timeout {
318 opts.pool_idle_timeout = Some(pool_idle_timeout);
319 }
320 if let Some(pool_max_idle_per_host) = root.pool_max_idle_per_host {
321 opts.pool_max_idle_per_host = Some(pool_max_idle_per_host);
322 }
323 if let Some(use_env_proxy) = root.use_env_proxy {
324 opts.use_env_proxy = use_env_proxy;
325 }
326
327 if config.contains_prefix("timeouts") {
329 let timeouts_config = config.prefix_view("timeouts");
330 opts.timeouts = match HttpTimeoutOptions::from_config(&timeouts_config) {
331 Ok(timeouts) => timeouts,
332 Err(error) => return Err(Self::resolve_config_error(&timeouts_config, error)),
333 };
334 }
335
336 if config.contains_prefix("proxy") {
338 let proxy_config = config.prefix_view("proxy");
339 opts.proxy = match ProxyOptions::from_config(&proxy_config) {
340 Ok(proxy) => proxy,
341 Err(error) => return Err(Self::resolve_config_error(&proxy_config, error)),
342 };
343 }
344
345 if config.contains_prefix("logging") {
347 let logging_config = config.prefix_view("logging");
348 opts.logging = match HttpLoggingOptions::from_config(&logging_config) {
349 Ok(logging) => logging,
350 Err(error) => return Err(Self::resolve_config_error(&logging_config, error)),
351 };
352 }
353
354 if config.contains_prefix("retry") {
355 let retry_config = config.prefix_view("retry");
356 opts.retry = match HttpRetryOptions::from_config(&retry_config) {
357 Ok(retry) => retry,
358 Err(error) => return Err(Self::resolve_config_error(&retry_config, error)),
359 };
360 }
361
362 if config.contains_prefix("sse") {
363 let sse_config = config.prefix_view("sse");
364 let sse = match Self::read_sse_config(&sse_config) {
365 Ok(sse) => sse,
366 Err(error) => {
367 return Err(Self::resolve_config_error(
368 &sse_config,
369 HttpConfigError::from(error),
370 ))
371 }
372 };
373 if let Some(mode) = sse.json_mode.as_deref() {
374 opts.sse_json_mode = match Self::parse_sse_json_mode(mode) {
375 Ok(mode) => mode,
376 Err(error) => return Err(Self::resolve_config_error(&sse_config, error)),
377 };
378 }
379 if let Some(marker) = sse.done_marker.as_deref() {
380 opts.sse_done_marker_policy = match Self::parse_sse_done_marker_policy(marker) {
381 Ok(marker) => marker,
382 Err(error) => return Err(Self::resolve_config_error(&sse_config, error)),
383 };
384 }
385 if let Some(max_line_bytes) = sse.max_line_bytes {
386 opts.sse_max_line_bytes =
387 match Self::validate_positive_limit("max_line_bytes", max_line_bytes) {
388 Ok(limit) => limit,
389 Err(error) => return Err(Self::resolve_config_error(&sse_config, error)),
390 };
391 }
392 if let Some(max_frame_bytes) = sse.max_frame_bytes {
393 opts.sse_max_frame_bytes =
394 match Self::validate_positive_limit("max_frame_bytes", max_frame_bytes) {
395 Ok(limit) => limit,
396 Err(error) => return Err(Self::resolve_config_error(&sse_config, error)),
397 };
398 }
399 }
400
401 let headers_prefix = "default_headers";
403 let full_headers_prefix = "default_headers.";
404 let mut header_map: HashMap<String, String> = HashMap::new();
405 for (k, _) in config.iter_prefix(full_headers_prefix) {
406 let header_name = &k[full_headers_prefix.len()..];
407 let value = match config.get_string(k) {
408 Ok(value) => value,
409 Err(error) => {
410 return Err(HttpConfigError::config_error(
411 config.resolve_key(k),
412 error.to_string(),
413 ))
414 }
415 };
416 header_map.insert(header_name.to_string(), value);
417 }
418 let json_headers = match config.get_optional_string(headers_prefix) {
420 Ok(json_headers) => json_headers,
421 Err(error) => {
422 return Err(Self::resolve_config_error(
423 config,
424 HttpConfigError::from(error),
425 ))
426 }
427 };
428 if !header_map.is_empty() && json_headers.is_some() {
429 return Err(HttpConfigError::invalid_value(
430 config.resolve_key(headers_prefix),
431 "default_headers sub-key form and JSON map form cannot be used at the same time",
432 ));
433 }
434 if let Some(json_str) = json_headers {
435 let parsed: HashMap<String, String> = match serde_json::from_str(&json_str) {
436 Ok(parsed) => parsed,
437 Err(error) => {
438 return Err(HttpConfigError::type_error(
439 config.resolve_key(headers_prefix),
440 format!("Failed to parse default_headers JSON: {error}"),
441 ))
442 }
443 };
444 header_map = parsed;
445 }
446 if !header_map.is_empty() {
447 opts.default_headers =
448 hashmap_to_headermap(&config.resolve_key(headers_prefix), header_map)?;
449 }
450
451 if let Some(names) = root.sensitive_headers {
452 let mut sh = SensitiveHttpHeaders::new();
453 sh.extend(names);
454 opts.sensitive_headers = sh;
455 }
456
457 Ok(opts)
458 }
459
460 pub fn validate(&self) -> Result<(), HttpConfigError> {
466 self.timeouts
467 .validate()
468 .map_err(|e| e.prepend_path_prefix("timeouts"))?;
469 self.proxy.validate()?;
470 self.logging.validate()?;
471 self.retry
472 .validate()
473 .map_err(|e| e.prepend_path_prefix("retry"))?;
474 Self::validate_positive_limit(
475 "error_response_preview_limit",
476 self.error_response_preview_limit,
477 )?;
478 if let Some(user_agent) = self.user_agent.as_deref() {
479 if user_agent.trim().is_empty() {
480 return Err(HttpConfigError::invalid_value(
481 "user_agent",
482 "Value cannot be empty",
483 ));
484 }
485 HeaderValue::from_str(user_agent).map_err(|error| {
486 HttpConfigError::invalid_value(
487 "user_agent",
488 format!("Invalid header value: {error}"),
489 )
490 })?;
491 }
492 Self::validate_positive_limit("sse.max_line_bytes", self.sse_max_line_bytes)?;
493 Self::validate_positive_limit("sse.max_frame_bytes", self.sse_max_frame_bytes)?;
494 Ok(())
495 }
496}