1use std::collections::HashMap;
11
12use http::HeaderMap;
13use qubit_config::{ConfigReader, ConfigResult};
14use url::Url;
15
16use super::from_config_helpers::hashmap_to_headermap;
17use super::http_retry_options::HttpRetryOptions;
18use super::logging_options::HttpLoggingOptions;
19use super::proxy_options::ProxyOptions;
20use super::sensitive_headers::SensitiveHeaders;
21use super::timeout_options::TimeoutOptions;
22use super::HttpConfigError;
23use crate::{
24 constants::{DEFAULT_SSE_MAX_FRAME_BYTES, DEFAULT_SSE_MAX_LINE_BYTES},
25 request::parse_header,
26 sse::SseJsonMode,
27 HttpResult,
28};
29
30#[derive(Debug, Clone)]
32pub struct HttpClientOptions {
33 pub base_url: Option<Url>,
35 pub default_headers: HeaderMap,
37 pub timeouts: TimeoutOptions,
39 pub proxy: ProxyOptions,
41 pub logging: HttpLoggingOptions,
43 pub retry: HttpRetryOptions,
45 pub sensitive_headers: SensitiveHeaders,
47 pub ipv4_only: bool,
49 pub sse_json_mode: SseJsonMode,
51 pub sse_max_line_bytes: usize,
53 pub sse_max_frame_bytes: usize,
55}
56
57impl Default for HttpClientOptions {
58 fn default() -> Self {
65 Self {
66 base_url: None,
67 default_headers: HeaderMap::new(),
68 timeouts: TimeoutOptions::default(),
69 proxy: ProxyOptions::default(),
70 logging: HttpLoggingOptions::default(),
71 retry: HttpRetryOptions::default(),
72 sensitive_headers: SensitiveHeaders::default(),
73 ipv4_only: false,
74 sse_json_mode: SseJsonMode::Lenient,
75 sse_max_line_bytes: DEFAULT_SSE_MAX_LINE_BYTES,
76 sse_max_frame_bytes: DEFAULT_SSE_MAX_FRAME_BYTES,
77 }
78 }
79}
80
81struct HttpClientRootConfigInput {
83 base_url: Option<String>,
84 ipv4_only: Option<bool>,
85 sensitive_headers: Option<Vec<String>>,
86}
87
88struct HttpClientSseConfigInput {
90 json_mode: Option<String>,
91 max_line_bytes: Option<usize>,
92 max_frame_bytes: Option<usize>,
93}
94
95impl HttpClientOptions {
96 fn resolve_config_error<R>(config: &R, mut error: HttpConfigError) -> HttpConfigError
97 where
98 R: ConfigReader + ?Sized,
99 {
100 error.path = if error.path.is_empty() {
101 config.resolve_key("")
102 } else {
103 config.resolve_key(&error.path)
104 };
105 error
106 }
107
108 fn read_config<R>(config: &R) -> ConfigResult<HttpClientRootConfigInput>
109 where
110 R: ConfigReader + ?Sized,
111 {
112 Ok(HttpClientRootConfigInput {
113 base_url: config.get_optional_string("base_url")?,
114 ipv4_only: config.get_optional("ipv4_only")?,
115 sensitive_headers: config.get_optional_string_list("sensitive_headers")?,
116 })
117 }
118
119 fn read_sse_config<R>(config: &R) -> ConfigResult<HttpClientSseConfigInput>
120 where
121 R: ConfigReader + ?Sized,
122 {
123 Ok(HttpClientSseConfigInput {
124 json_mode: config.get_optional_string("json_mode")?,
125 max_line_bytes: config.get_optional("max_line_bytes")?,
126 max_frame_bytes: config.get_optional("max_frame_bytes")?,
127 })
128 }
129
130 fn parse_base_url(base_url: &str) -> Result<Url, HttpConfigError> {
131 Url::parse(base_url).map_err(|error| {
132 HttpConfigError::invalid_value("base_url", format!("Invalid URL: {error}"))
133 })
134 }
135
136 fn parse_sse_json_mode(value: &str) -> Result<SseJsonMode, HttpConfigError> {
137 let normalized = value.trim().to_ascii_uppercase().replace('-', "_");
138 match normalized.as_str() {
139 "LENIENT" => Ok(SseJsonMode::Lenient),
140 "STRICT" => Ok(SseJsonMode::Strict),
141 _ => Err(HttpConfigError::invalid_value(
142 "json_mode",
143 format!("Unsupported SSE JSON mode: {value}"),
144 )),
145 }
146 }
147
148 fn validate_sse_limit(path: &str, value: usize) -> Result<usize, HttpConfigError> {
149 if value == 0 {
150 return Err(HttpConfigError::invalid_value(
151 path,
152 "Value must be greater than 0",
153 ));
154 }
155 Ok(value)
156 }
157
158 pub fn new() -> Self {
163 Self::default()
164 }
165
166 pub fn set_base_url(&mut self, base_url: &str) -> Result<&mut Self, HttpConfigError> {
174 let parsed = Self::parse_base_url(base_url)?;
175 self.base_url = Some(parsed);
176 Ok(self)
177 }
178
179 pub fn add_header(&mut self, name: &str, value: &str) -> HttpResult<&mut Self> {
188 let (header_name, header_value) = parse_header(name, value)?;
189 self.default_headers.insert(header_name, header_value);
190 Ok(self)
191 }
192
193 pub fn add_headers<'a, I>(&mut self, headers: I) -> HttpResult<&mut Self>
203 where
204 I: IntoIterator<Item = (&'a str, &'a str)>,
205 {
206 let mut parsed_headers = HeaderMap::new();
207 for (name, value) in headers {
208 let (header_name, header_value) = parse_header(name, value)?;
209 parsed_headers.insert(header_name, header_value);
210 }
211 self.default_headers.extend(parsed_headers);
212 Ok(self)
213 }
214
215 pub fn from_config<R>(config: &R) -> Result<Self, HttpConfigError>
224 where
225 R: ConfigReader + ?Sized,
226 {
227 let mut opts = HttpClientOptions::default();
228
229 let root = Self::read_config(config)
230 .map_err(HttpConfigError::from)
231 .map_err(|e| Self::resolve_config_error(config, e))?;
232
233 if let Some(s) = root.base_url {
234 opts.set_base_url(&s)
235 .map_err(|e| Self::resolve_config_error(config, e))?;
236 }
237
238 if let Some(v) = root.ipv4_only {
239 opts.ipv4_only = v;
240 }
241
242 if config.contains_prefix("timeouts") {
244 let timeouts_config = config.prefix_view("timeouts");
245 opts.timeouts = TimeoutOptions::from_config(&timeouts_config)
246 .map_err(|e| Self::resolve_config_error(&timeouts_config, e))?;
247 }
248
249 if config.contains_prefix("proxy") {
251 let proxy_config = config.prefix_view("proxy");
252 opts.proxy = ProxyOptions::from_config(&proxy_config)
253 .map_err(|e| Self::resolve_config_error(&proxy_config, e))?;
254 }
255
256 if config.contains_prefix("logging") {
258 let logging_config = config.prefix_view("logging");
259 opts.logging = HttpLoggingOptions::from_config(&logging_config)
260 .map_err(|e| Self::resolve_config_error(&logging_config, e))?;
261 }
262
263 if config.contains_prefix("retry") {
264 let retry_config = config.prefix_view("retry");
265 opts.retry = HttpRetryOptions::from_config(&retry_config)
266 .map_err(|e| Self::resolve_config_error(&retry_config, e))?;
267 }
268
269 if config.contains_prefix("sse") {
270 let sse_config = config.prefix_view("sse");
271 let sse = Self::read_sse_config(&sse_config)
272 .map_err(HttpConfigError::from)
273 .map_err(|e| Self::resolve_config_error(&sse_config, e))?;
274 if let Some(mode) = sse.json_mode.as_deref() {
275 opts.sse_json_mode = Self::parse_sse_json_mode(mode)
276 .map_err(|e| Self::resolve_config_error(&sse_config, e))?;
277 }
278 if let Some(max_line_bytes) = sse.max_line_bytes {
279 opts.sse_max_line_bytes =
280 Self::validate_sse_limit("max_line_bytes", max_line_bytes)
281 .map_err(|e| Self::resolve_config_error(&sse_config, e))?;
282 }
283 if let Some(max_frame_bytes) = sse.max_frame_bytes {
284 opts.sse_max_frame_bytes =
285 Self::validate_sse_limit("max_frame_bytes", max_frame_bytes)
286 .map_err(|e| Self::resolve_config_error(&sse_config, e))?;
287 }
288 }
289
290 let headers_prefix = "default_headers";
292 let full_headers_prefix = "default_headers.";
293 let mut header_map: HashMap<String, String> = HashMap::new();
294 for (k, _) in config.iter_prefix(full_headers_prefix) {
295 let header_name = &k[full_headers_prefix.len()..];
296 let value = config
297 .get_string(k)
298 .map_err(|e| HttpConfigError::config_error(config.resolve_key(k), e.to_string()))?;
299 header_map.insert(header_name.to_string(), value);
300 }
301 if header_map.is_empty() {
303 if let Some(json_str) = config
304 .get_optional_string(headers_prefix)
305 .map_err(HttpConfigError::from)
306 .map_err(|e| Self::resolve_config_error(config, e))?
307 {
308 let parsed: HashMap<String, String> =
309 serde_json::from_str(&json_str).map_err(|e| {
310 HttpConfigError::type_error(
311 config.resolve_key(headers_prefix),
312 format!("Failed to parse default_headers JSON: {e}"),
313 )
314 })?;
315 header_map = parsed;
316 }
317 }
318 if !header_map.is_empty() {
319 opts.default_headers = hashmap_to_headermap(headers_prefix, header_map)?;
320 }
321
322 if let Some(names) = root.sensitive_headers {
323 let mut sh = SensitiveHeaders::new();
324 sh.extend(names);
325 opts.sensitive_headers = sh;
326 }
327
328 Ok(opts)
329 }
330
331 pub fn validate(&self) -> Result<(), HttpConfigError> {
337 self.proxy.validate()?;
338 self.logging.validate()?;
339 self.retry
340 .validate()
341 .map_err(|e| e.prepend_path_prefix("retry"))?;
342 Self::validate_sse_limit("sse.max_line_bytes", self.sse_max_line_bytes)?;
343 Self::validate_sse_limit("sse.max_frame_bytes", self.sse_max_frame_bytes)?;
344 Ok(())
345 }
346}