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::{
33 AsyncHttpHeaderInjector,
34 HttpClient,
35 HttpError,
36 HttpHeaderInjector,
37 HttpRequestBodyByteStream,
38 HttpRequestStreamingBody,
39 HttpResult,
40 HttpRetryMethodPolicy,
41 LogSanitizePolicy,
42 LogSanitizer,
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 sanitizer = LogSanitizer::for_debug(&self.log_sanitize_policy);
93 let url = self
94 .debug_resolved_url()
95 .map(|url| sanitizer.sanitize_url(&url));
96 let base_url = self
97 .base_url
98 .as_ref()
99 .map(|url| sanitizer.sanitize_url(url));
100 formatter
101 .debug_struct("HttpRequestBuilder")
102 .field("method", &self.method)
103 .field("url", &url)
104 .field("headers", &sanitizer.sanitize_header_map(&self.headers))
105 .field("body", &self.body)
106 .field(
107 "streaming_body",
108 &self.streaming_body.as_ref().map(|_| "present"),
109 )
110 .field("request_timeout", &self.request_timeout)
111 .field("write_timeout", &self.write_timeout)
112 .field("read_timeout", &self.read_timeout)
113 .field("base_url", &base_url)
114 .field("ipv4_only", &self.ipv4_only)
115 .field(
116 "cancellation_token_present",
117 &self.cancellation_token.is_some(),
118 )
119 .field("retry_override", &self.retry_override)
120 .field(
121 "default_headers",
122 &sanitizer.sanitize_header_map(&self.default_headers),
123 )
124 .field("injector_count", &self.injectors.len())
125 .field("async_injector_count", &self.async_injectors.len())
126 .finish()
127 }
128}
129
130impl HttpRequestBuilder {
131 pub(crate) fn new(method: Method, path: &str, client: &HttpClient) -> Self {
141 let options = client.options();
142 Self {
143 method,
144 path: path.to_string(),
145 query: Vec::new(),
146 headers: HeaderMap::new(),
147 body: HttpRequestBody::Empty,
148 streaming_body: None,
149 request_timeout: options.timeouts.request_timeout,
150 write_timeout: options.timeouts.write_timeout,
151 read_timeout: options.timeouts.read_timeout,
152 base_url: options.base_url.clone(),
153 ipv4_only: options.ipv4_only,
154 cancellation_token: None,
155 retry_override: HttpRequestRetryOverride::default(),
156 default_headers: client.headers_snapshot(),
157 injectors: client.injectors_snapshot(),
158 async_injectors: client.async_injectors_snapshot(),
159 log_sanitize_policy: options.log_sanitize_policy.clone(),
160 }
161 }
162
163 pub fn query_param(mut self, key: &str, value: &str) -> Self {
172 self.query.push((key.to_string(), value.to_string()));
173 self
174 }
175
176 pub fn query_params<'a, I>(mut self, params: I) -> Self
184 where
185 I: IntoIterator<Item = (&'a str, &'a str)>,
186 {
187 for (key, value) in params {
188 self = self.query_param(key, value);
189 }
190 self
191 }
192
193 pub fn header(mut self, name: &str, value: &str) -> HttpResult<Self> {
202 let (header_name, header_value) = parse_header(name, value)?;
203 self.headers.insert(header_name, header_value);
204 Ok(self)
205 }
206
207 pub fn headers(mut self, headers: HeaderMap) -> Self {
215 self.headers.extend(headers);
216 self
217 }
218
219 pub fn bytes_body(mut self, body: impl Into<Bytes>) -> Self {
227 self.body = HttpRequestBody::Bytes(body.into());
228 self.streaming_body = None;
229 self
230 }
231
232 pub fn stream_body<I, B>(mut self, chunks: I) -> Self
240 where
241 I: IntoIterator<Item = B>,
242 B: Into<Bytes>,
243 {
244 self.body = HttpRequestBody::Stream(chunks.into_iter().map(Into::into).collect());
245 self.streaming_body = None;
246 self
247 }
248
249 pub fn streaming_body<F>(mut self, factory: F) -> Self
260 where
261 F: Fn() -> Pin<Box<dyn Future<Output = HttpRequestBodyByteStream> + Send + 'static>>
262 + Send
263 + Sync
264 + 'static,
265 {
266 self.streaming_body = Some(HttpRequestStreamingBody::new(factory));
267 self.body = HttpRequestBody::Empty;
268 self
269 }
270
271 pub fn text_body(mut self, body: impl Into<String>) -> Self {
279 if !self.headers.contains_key(CONTENT_TYPE) {
280 self.headers.insert(
281 CONTENT_TYPE,
282 HeaderValue::from_static("text/plain; charset=utf-8"),
283 );
284 }
285 self.body = HttpRequestBody::Text(body.into());
286 self.streaming_body = None;
287 self
288 }
289
290 pub fn json_body<T>(mut self, value: &T) -> HttpResult<Self>
298 where
299 T: Serialize,
300 {
301 let bytes = serde_json::to_vec(value)
302 .map_err(|error| HttpError::decode(format!("Failed to encode JSON body: {}", error)))?;
303 if !self.headers.contains_key(CONTENT_TYPE) {
304 self.headers
305 .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
306 }
307 self.body = HttpRequestBody::Json(Bytes::from(bytes));
308 self.streaming_body = None;
309 Ok(self)
310 }
311
312 pub fn form_body<'a, I>(mut self, fields: I) -> Self
320 where
321 I: IntoIterator<Item = (&'a str, &'a str)>,
322 {
323 let mut serializer = form_urlencoded::Serializer::new(String::new());
324 for (key, value) in fields {
325 serializer.append_pair(key, value);
326 }
327 let body = serializer.finish();
328 if !self.headers.contains_key(CONTENT_TYPE) {
329 self.headers.insert(
330 CONTENT_TYPE,
331 HeaderValue::from_static("application/x-www-form-urlencoded"),
332 );
333 }
334 self.body = HttpRequestBody::Form(Bytes::from(body));
335 self.streaming_body = None;
336 self
337 }
338
339 pub fn multipart_body(mut self, body: impl Into<Bytes>, boundary: &str) -> HttpResult<Self> {
353 if !content_type::is_valid_multipart_boundary(boundary) {
354 return Err(HttpError::other(
355 "Invalid multipart boundary for multipart_body: expected 1 to 70 token-safe ASCII characters",
356 ));
357 }
358 if let Some(existing) = self.headers.get(CONTENT_TYPE) {
359 let existing = existing.to_str().map_err(|error| {
360 HttpError::other(format!(
361 "Existing multipart Content-Type must be valid UTF-8: {error}"
362 ))
363 })?;
364 if !content_type::is_multipart(existing) {
365 return Err(HttpError::other(
366 "Existing Content-Type must be multipart when using multipart_body",
367 ));
368 }
369 let declares_boundary = content_type::has_parameter_name(existing, "boundary")
370 .ok_or_else(|| {
371 HttpError::other(
372 "Existing multipart Content-Type boundary is malformed or invalid",
373 )
374 })?;
375 if let Some(existing_boundary) = content_type::parameter(existing, "boundary") {
376 if !content_type::is_valid_multipart_boundary(&existing_boundary) {
377 return Err(HttpError::other(
378 "Existing multipart Content-Type boundary is malformed or invalid",
379 ));
380 }
381 if existing_boundary != boundary {
382 return Err(HttpError::other(format!(
383 "Existing multipart Content-Type boundary '{existing_boundary}' does not match multipart_body boundary '{boundary}'"
384 )));
385 }
386 } else if declares_boundary {
387 return Err(HttpError::other(
388 "Existing multipart Content-Type boundary is malformed or invalid",
389 ));
390 } else {
391 let value = existing.trim().trim_end_matches(';').trim_end();
392 let value = HeaderValue::from_str(&format!("{value}; boundary={boundary}"))
393 .expect("validated multipart boundary should build a valid Content-Type");
394 self.headers.insert(CONTENT_TYPE, value);
395 }
396 } else {
397 let value = HeaderValue::from_str(&format!("multipart/form-data; boundary={boundary}"))
398 .expect("validated multipart boundary should build a valid Content-Type");
399 self.headers.insert(CONTENT_TYPE, value);
400 }
401 self.body = HttpRequestBody::Multipart(body.into());
402 self.streaming_body = None;
403 Ok(self)
404 }
405
406 pub fn ndjson_body<T>(mut self, records: &[T]) -> HttpResult<Self>
417 where
418 T: Serialize,
419 {
420 let mut payload = String::new();
421 for record in records {
422 let line = serde_json::to_string(record).map_err(|error| {
423 HttpError::decode(format!("Failed to encode NDJSON record: {error}"))
424 })?;
425 payload.push_str(&line);
426 payload.push('\n');
427 }
428 if !self.headers.contains_key(CONTENT_TYPE) {
429 self.headers.insert(
430 CONTENT_TYPE,
431 HeaderValue::from_static("application/x-ndjson"),
432 );
433 }
434 self.body = HttpRequestBody::Ndjson(Bytes::from(payload));
435 self.streaming_body = None;
436 Ok(self)
437 }
438
439 pub fn request_timeout(mut self, timeout: Duration) -> HttpResult<Self> {
453 validate_positive_timeout("request_timeout", timeout)?;
454 self.request_timeout = Some(timeout);
455 Ok(self)
456 }
457
458 pub fn write_timeout(mut self, timeout: Duration) -> HttpResult<Self> {
469 validate_positive_timeout("write_timeout", timeout)?;
470 self.write_timeout = timeout;
471 Ok(self)
472 }
473
474 pub fn read_timeout(mut self, timeout: Duration) -> HttpResult<Self> {
485 validate_positive_timeout("read_timeout", timeout)?;
486 self.read_timeout = timeout;
487 Ok(self)
488 }
489
490 pub fn base_url(mut self, base_url: Url) -> Self {
498 self.base_url = Some(base_url);
499 self
500 }
501
502 pub fn clear_base_url(mut self) -> Self {
507 self.base_url = None;
508 self
509 }
510
511 pub fn ipv4_only(mut self, enabled: bool) -> Self {
519 self.ipv4_only = enabled;
520 self
521 }
522
523 pub fn cancellation_token(mut self, token: CancellationToken) -> Self {
531 self.cancellation_token = Some(token);
532 self
533 }
534
535 pub fn force_retry(mut self) -> Self {
540 self.retry_override = self.retry_override.force_enable();
541 self
542 }
543
544 pub fn disable_retry(mut self) -> Self {
549 self.retry_override = self.retry_override.force_disable();
550 self
551 }
552
553 pub fn retry_method_policy(mut self, policy: HttpRetryMethodPolicy) -> Self {
561 self.retry_override = self.retry_override.with_method_policy(policy);
562 self
563 }
564
565 pub fn honor_retry_after(mut self, enabled: bool) -> Self {
574 self.retry_override = self.retry_override.with_honor_retry_after(enabled);
575 self
576 }
577
578 pub fn build(self) -> HttpRequest {
583 HttpRequest::new(self)
584 }
585
586 fn debug_resolved_url(&self) -> Option<Url> {
591 let mut url = match Url::parse(&self.path) {
592 Ok(url) => url,
593 Err(_) => self.base_url.as_ref()?.join(&self.path).ok()?,
594 };
595 if !self.query.is_empty() {
596 let mut pairs = url.query_pairs_mut();
597 for (key, value) in &self.query {
598 pairs.append_pair(key, value);
599 }
600 }
601 Some(url)
602 }
603}