1use std::time::Duration;
13use std::{
14 fmt,
15 future::Future,
16 pin::Pin,
17};
18
19use bytes::Bytes;
20use http::header::CONTENT_TYPE;
21use http::{
22 HeaderMap,
23 HeaderValue,
24 Method,
25};
26use serde::Serialize;
27use tokio_util::sync::CancellationToken;
28use url::form_urlencoded;
29use url::Url;
30
31use crate::content_type;
32use crate::sanitize::SanitizedDebugger;
33use crate::{
34 AsyncHttpHeaderInjector,
35 HttpClient,
36 HttpError,
37 HttpHeaderInjector,
38 HttpRequestBodyByteStream,
39 HttpRequestStreamingBody,
40 HttpResult,
41 HttpRetryMethodPolicy,
42 LogSanitizePolicy,
43};
44
45use super::http_request::HttpRequest;
46use super::http_request_body::HttpRequestBody;
47use super::http_request_retry_override::HttpRequestRetryOverride;
48use super::parse_header;
49use super::validate_positive_timeout;
50
51#[derive(Clone)]
53pub struct HttpRequestBuilder {
54 pub(super) method: Method,
56 pub(super) path: String,
58 pub(super) query: Vec<(String, String)>,
60 pub(super) headers: HeaderMap,
62 pub(super) body: HttpRequestBody,
64 pub(super) streaming_body: Option<HttpRequestStreamingBody>,
66 pub(super) request_timeout: Option<Duration>,
68 pub(super) write_timeout: Duration,
70 pub(super) read_timeout: Duration,
72 pub(super) base_url: Option<Url>,
74 pub(super) ipv4_only: bool,
76 pub(super) cancellation_token: Option<CancellationToken>,
78 pub(super) retry_override: HttpRequestRetryOverride,
80 pub(super) default_headers: HeaderMap,
82 pub(super) injectors: Vec<HttpHeaderInjector>,
84 pub(super) async_injectors: Vec<AsyncHttpHeaderInjector>,
86 pub(super) log_sanitize_policy: LogSanitizePolicy,
88}
89
90impl fmt::Debug for HttpRequestBuilder {
91 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
92 let debugger = SanitizedDebugger::new(&self.log_sanitize_policy);
93 let url = self.debug_resolved_url().map(|url| debugger.url(&url));
94 let base_url = self.base_url.as_ref().map(|url| debugger.url(url));
95 formatter
96 .debug_struct("HttpRequestBuilder")
97 .field("method", &self.method)
98 .field("url", &url)
99 .field("headers", &debugger.headers(&self.headers))
100 .field("body", &self.body)
101 .field(
102 "streaming_body",
103 &self.streaming_body.as_ref().map(|_| "present"),
104 )
105 .field("request_timeout", &self.request_timeout)
106 .field("write_timeout", &self.write_timeout)
107 .field("read_timeout", &self.read_timeout)
108 .field("base_url", &base_url)
109 .field("ipv4_only", &self.ipv4_only)
110 .field(
111 "cancellation_token_present",
112 &self.cancellation_token.is_some(),
113 )
114 .field("retry_override", &self.retry_override)
115 .field("default_headers", &debugger.headers(&self.default_headers))
116 .field("injector_count", &self.injectors.len())
117 .field("async_injector_count", &self.async_injectors.len())
118 .finish()
119 }
120}
121
122impl HttpRequestBuilder {
123 pub(crate) fn new(method: Method, path: &str, client: &HttpClient) -> Self {
133 let options = client.options();
134 Self {
135 method,
136 path: path.to_string(),
137 query: Vec::new(),
138 headers: HeaderMap::new(),
139 body: HttpRequestBody::Empty,
140 streaming_body: None,
141 request_timeout: options.timeouts.request_timeout,
142 write_timeout: options.timeouts.write_timeout,
143 read_timeout: options.timeouts.read_timeout,
144 base_url: options.base_url.clone(),
145 ipv4_only: options.ipv4_only,
146 cancellation_token: None,
147 retry_override: HttpRequestRetryOverride::default(),
148 default_headers: client.headers_snapshot(),
149 injectors: client.injectors_snapshot(),
150 async_injectors: client.async_injectors_snapshot(),
151 log_sanitize_policy: options.log_sanitize_policy.clone(),
152 }
153 }
154
155 pub fn query_param(mut self, key: &str, value: &str) -> Self {
164 self.query.push((key.to_string(), value.to_string()));
165 self
166 }
167
168 pub fn query_params<'a, I>(mut self, params: I) -> Self
176 where
177 I: IntoIterator<Item = (&'a str, &'a str)>,
178 {
179 for (key, value) in params {
180 self = self.query_param(key, value);
181 }
182 self
183 }
184
185 pub fn header(mut self, name: &str, value: &str) -> HttpResult<Self> {
194 let (header_name, header_value) = parse_header(name, value)?;
195 self.headers.insert(header_name, header_value);
196 Ok(self)
197 }
198
199 pub fn headers(mut self, headers: HeaderMap) -> Self {
207 self.headers.extend(headers);
208 self
209 }
210
211 pub fn bytes_body(mut self, body: impl Into<Bytes>) -> Self {
219 self.body = HttpRequestBody::Bytes(body.into());
220 self.streaming_body = None;
221 self
222 }
223
224 pub fn stream_body<I, B>(mut self, chunks: I) -> Self
232 where
233 I: IntoIterator<Item = B>,
234 B: Into<Bytes>,
235 {
236 self.body = HttpRequestBody::Stream(chunks.into_iter().map(Into::into).collect());
237 self.streaming_body = None;
238 self
239 }
240
241 pub fn streaming_body<F>(mut self, factory: F) -> Self
252 where
253 F: Fn() -> Pin<Box<dyn Future<Output = HttpRequestBodyByteStream> + Send + 'static>>
254 + Send
255 + Sync
256 + 'static,
257 {
258 self.streaming_body = Some(HttpRequestStreamingBody::new(factory));
259 self.body = HttpRequestBody::Empty;
260 self
261 }
262
263 pub fn text_body(mut self, body: impl Into<String>) -> Self {
271 if !self.headers.contains_key(CONTENT_TYPE) {
272 self.headers.insert(
273 CONTENT_TYPE,
274 HeaderValue::from_static("text/plain; charset=utf-8"),
275 );
276 }
277 self.body = HttpRequestBody::Text(body.into());
278 self.streaming_body = None;
279 self
280 }
281
282 pub fn json_body<T>(mut self, value: &T) -> HttpResult<Self>
290 where
291 T: Serialize,
292 {
293 let bytes = serde_json::to_vec(value)
294 .map_err(|error| HttpError::decode(format!("Failed to encode JSON body: {}", error)))?;
295 if !self.headers.contains_key(CONTENT_TYPE) {
296 self.headers
297 .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
298 }
299 self.body = HttpRequestBody::Json(Bytes::from(bytes));
300 self.streaming_body = None;
301 Ok(self)
302 }
303
304 pub fn form_body<'a, I>(mut self, fields: I) -> Self
312 where
313 I: IntoIterator<Item = (&'a str, &'a str)>,
314 {
315 let mut serializer = form_urlencoded::Serializer::new(String::new());
316 for (key, value) in fields {
317 serializer.append_pair(key, value);
318 }
319 let body = serializer.finish();
320 if !self.headers.contains_key(CONTENT_TYPE) {
321 self.headers.insert(
322 CONTENT_TYPE,
323 HeaderValue::from_static("application/x-www-form-urlencoded"),
324 );
325 }
326 self.body = HttpRequestBody::Form(Bytes::from(body));
327 self.streaming_body = None;
328 self
329 }
330
331 pub fn multipart_body(mut self, body: impl Into<Bytes>, boundary: &str) -> HttpResult<Self> {
345 if !content_type::is_valid_multipart_boundary(boundary) {
346 return Err(HttpError::other(
347 "Invalid multipart boundary for multipart_body: expected 1 to 70 token-safe ASCII characters",
348 ));
349 }
350 if let Some(existing) = self.headers.get(CONTENT_TYPE) {
351 let existing = existing.to_str().map_err(|error| {
352 HttpError::other(format!(
353 "Existing multipart Content-Type must be valid UTF-8: {error}"
354 ))
355 })?;
356 if !content_type::is_multipart(existing) {
357 return Err(HttpError::other(
358 "Existing Content-Type must be multipart when using multipart_body",
359 ));
360 }
361 let declares_boundary = content_type::has_parameter_name(existing, "boundary")
362 .ok_or_else(|| {
363 HttpError::other(
364 "Existing multipart Content-Type boundary is malformed or invalid",
365 )
366 })?;
367 if let Some(existing_boundary) = content_type::parameter(existing, "boundary") {
368 if !content_type::is_valid_multipart_boundary(&existing_boundary) {
369 return Err(HttpError::other(
370 "Existing multipart Content-Type boundary is malformed or invalid",
371 ));
372 }
373 if existing_boundary != boundary {
374 return Err(HttpError::other(format!(
375 "Existing multipart Content-Type boundary '{existing_boundary}' does not match multipart_body boundary '{boundary}'"
376 )));
377 }
378 } else if declares_boundary {
379 return Err(HttpError::other(
380 "Existing multipart Content-Type boundary is malformed or invalid",
381 ));
382 } else {
383 let value = existing.trim().trim_end_matches(';').trim_end();
384 let value = HeaderValue::from_str(&format!("{value}; boundary={boundary}"))
385 .expect("validated multipart boundary should build a valid Content-Type");
386 self.headers.insert(CONTENT_TYPE, value);
387 }
388 } else {
389 let value = HeaderValue::from_str(&format!("multipart/form-data; boundary={boundary}"))
390 .expect("validated multipart boundary should build a valid Content-Type");
391 self.headers.insert(CONTENT_TYPE, value);
392 }
393 self.body = HttpRequestBody::Multipart(body.into());
394 self.streaming_body = None;
395 Ok(self)
396 }
397
398 pub fn ndjson_body<T>(mut self, records: &[T]) -> HttpResult<Self>
409 where
410 T: Serialize,
411 {
412 let mut payload = String::new();
413 for record in records {
414 let line = serde_json::to_string(record).map_err(|error| {
415 HttpError::decode(format!("Failed to encode NDJSON record: {error}"))
416 })?;
417 payload.push_str(&line);
418 payload.push('\n');
419 }
420 if !self.headers.contains_key(CONTENT_TYPE) {
421 self.headers.insert(
422 CONTENT_TYPE,
423 HeaderValue::from_static("application/x-ndjson"),
424 );
425 }
426 self.body = HttpRequestBody::Ndjson(Bytes::from(payload));
427 self.streaming_body = None;
428 Ok(self)
429 }
430
431 pub fn request_timeout(mut self, timeout: Duration) -> HttpResult<Self> {
445 validate_positive_timeout("request_timeout", timeout)?;
446 self.request_timeout = Some(timeout);
447 Ok(self)
448 }
449
450 pub fn write_timeout(mut self, timeout: Duration) -> HttpResult<Self> {
461 validate_positive_timeout("write_timeout", timeout)?;
462 self.write_timeout = timeout;
463 Ok(self)
464 }
465
466 pub fn read_timeout(mut self, timeout: Duration) -> HttpResult<Self> {
477 validate_positive_timeout("read_timeout", timeout)?;
478 self.read_timeout = timeout;
479 Ok(self)
480 }
481
482 pub fn base_url(mut self, base_url: Url) -> Self {
490 self.base_url = Some(base_url);
491 self
492 }
493
494 pub fn clear_base_url(mut self) -> Self {
499 self.base_url = None;
500 self
501 }
502
503 pub fn ipv4_only(mut self, enabled: bool) -> Self {
511 self.ipv4_only = enabled;
512 self
513 }
514
515 pub fn cancellation_token(mut self, token: CancellationToken) -> Self {
523 self.cancellation_token = Some(token);
524 self
525 }
526
527 pub fn force_retry(mut self) -> Self {
532 self.retry_override = self.retry_override.force_enable();
533 self
534 }
535
536 pub fn disable_retry(mut self) -> Self {
541 self.retry_override = self.retry_override.force_disable();
542 self
543 }
544
545 pub fn retry_method_policy(mut self, policy: HttpRetryMethodPolicy) -> Self {
553 self.retry_override = self.retry_override.with_method_policy(policy);
554 self
555 }
556
557 pub fn honor_retry_after(mut self, enabled: bool) -> Self {
566 self.retry_override = self.retry_override.with_honor_retry_after(enabled);
567 self
568 }
569
570 pub fn build(self) -> HttpRequest {
575 HttpRequest::new(self)
576 }
577
578 fn debug_resolved_url(&self) -> Option<Url> {
583 let mut url = match Url::parse(&self.path) {
584 Ok(url) => url,
585 Err(_) => self.base_url.as_ref()?.join(&self.path).ok()?,
586 };
587 if !self.query.is_empty() {
588 let mut pairs = url.query_pairs_mut();
589 for (key, value) in &self.query {
590 pairs.append_pair(key, value);
591 }
592 }
593 Some(url)
594 }
595}