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("streaming_body", &self.streaming_body.as_ref().map(|_| "present"))
102 .field("request_timeout", &self.request_timeout)
103 .field("write_timeout", &self.write_timeout)
104 .field("read_timeout", &self.read_timeout)
105 .field("base_url", &base_url)
106 .field("ipv4_only", &self.ipv4_only)
107 .field("cancellation_token_present", &self.cancellation_token.is_some())
108 .field("retry_override", &self.retry_override)
109 .field("default_headers", &debugger.headers(&self.default_headers))
110 .field("injector_count", &self.injectors.len())
111 .field("async_injector_count", &self.async_injectors.len())
112 .finish()
113 }
114}
115
116impl HttpRequestBuilder {
117 pub(crate) fn new(method: Method, path: &str, client: &HttpClient) -> Self {
127 let options = client.options();
128 Self {
129 method,
130 path: path.to_string(),
131 query: Vec::new(),
132 headers: HeaderMap::new(),
133 body: HttpRequestBody::Empty,
134 streaming_body: None,
135 request_timeout: options.timeouts.request_timeout,
136 write_timeout: options.timeouts.write_timeout,
137 read_timeout: options.timeouts.read_timeout,
138 base_url: options.base_url.clone(),
139 ipv4_only: options.ipv4_only,
140 cancellation_token: None,
141 retry_override: HttpRequestRetryOverride::default(),
142 default_headers: client.headers_snapshot(),
143 injectors: client.injectors_snapshot(),
144 async_injectors: client.async_injectors_snapshot(),
145 log_sanitize_policy: options.log_sanitize_policy.clone(),
146 }
147 }
148
149 pub fn query_param(mut self, key: &str, value: &str) -> Self {
158 self.query.push((key.to_string(), value.to_string()));
159 self
160 }
161
162 pub fn query_params<'a, I>(mut self, params: I) -> Self
170 where
171 I: IntoIterator<Item = (&'a str, &'a str)>,
172 {
173 for (key, value) in params {
174 self = self.query_param(key, value);
175 }
176 self
177 }
178
179 pub fn header(mut self, name: &str, value: &str) -> HttpResult<Self> {
188 let (header_name, header_value) = parse_header(name, value)?;
189 self.headers.insert(header_name, header_value);
190 Ok(self)
191 }
192
193 pub fn headers(mut self, headers: HeaderMap) -> Self {
201 self.headers.extend(headers);
202 self
203 }
204
205 pub fn bytes_body(mut self, body: impl Into<Bytes>) -> Self {
213 self.body = HttpRequestBody::Bytes(body.into());
214 self.streaming_body = None;
215 self
216 }
217
218 pub fn stream_body<I, B>(mut self, chunks: I) -> Self
226 where
227 I: IntoIterator<Item = B>,
228 B: Into<Bytes>,
229 {
230 self.body = HttpRequestBody::Stream(chunks.into_iter().map(Into::into).collect());
231 self.streaming_body = None;
232 self
233 }
234
235 pub fn streaming_body<F>(mut self, factory: F) -> Self
246 where
247 F: Fn() -> Pin<Box<dyn Future<Output = HttpRequestBodyByteStream> + Send + 'static>> + Send + Sync + 'static,
248 {
249 self.streaming_body = Some(HttpRequestStreamingBody::new(factory));
250 self.body = HttpRequestBody::Empty;
251 self
252 }
253
254 pub fn text_body(mut self, body: impl Into<String>) -> Self {
262 if !self.headers.contains_key(CONTENT_TYPE) {
263 self.headers
264 .insert(CONTENT_TYPE, HeaderValue::from_static("text/plain; charset=utf-8"));
265 }
266 self.body = HttpRequestBody::Text(body.into());
267 self.streaming_body = None;
268 self
269 }
270
271 pub fn json_body<T>(mut self, value: &T) -> HttpResult<Self>
279 where
280 T: Serialize,
281 {
282 let bytes = serde_json::to_vec(value)
283 .map_err(|error| HttpError::decode(format!("Failed to encode JSON body: {}", error)))?;
284 if !self.headers.contains_key(CONTENT_TYPE) {
285 self.headers
286 .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
287 }
288 self.body = HttpRequestBody::Json(Bytes::from(bytes));
289 self.streaming_body = None;
290 Ok(self)
291 }
292
293 pub fn form_body<'a, I>(mut self, fields: I) -> Self
301 where
302 I: IntoIterator<Item = (&'a str, &'a str)>,
303 {
304 let mut serializer = form_urlencoded::Serializer::new(String::new());
305 for (key, value) in fields {
306 serializer.append_pair(key, value);
307 }
308 let body = serializer.finish();
309 if !self.headers.contains_key(CONTENT_TYPE) {
310 self.headers.insert(
311 CONTENT_TYPE,
312 HeaderValue::from_static("application/x-www-form-urlencoded"),
313 );
314 }
315 self.body = HttpRequestBody::Form(Bytes::from(body));
316 self.streaming_body = None;
317 self
318 }
319
320 pub fn multipart_body(mut self, body: impl Into<Bytes>, boundary: &str) -> HttpResult<Self> {
334 if !content_type::is_valid_multipart_boundary(boundary) {
335 return Err(HttpError::other(
336 "Invalid multipart boundary for multipart_body: expected 1 to 70 token-safe ASCII characters",
337 ));
338 }
339 if let Some(existing) = self.headers.get(CONTENT_TYPE) {
340 let existing = existing.to_str().map_err(|error| {
341 HttpError::other(format!("Existing multipart Content-Type must be valid UTF-8: {error}"))
342 })?;
343 if !content_type::is_multipart(existing) {
344 return Err(HttpError::other(
345 "Existing Content-Type must be multipart when using multipart_body",
346 ));
347 }
348 let declares_boundary = content_type::has_parameter_name(existing, "boundary")
349 .ok_or_else(|| HttpError::other("Existing multipart Content-Type boundary is malformed or invalid"))?;
350 if let Some(existing_boundary) = content_type::parameter(existing, "boundary") {
351 if !content_type::is_valid_multipart_boundary(&existing_boundary) {
352 return Err(HttpError::other(
353 "Existing multipart Content-Type boundary is malformed or invalid",
354 ));
355 }
356 if existing_boundary != boundary {
357 return Err(HttpError::other(format!(
358 "Existing multipart Content-Type boundary '{existing_boundary}' does not match multipart_body boundary '{boundary}'"
359 )));
360 }
361 } else if declares_boundary {
362 return Err(HttpError::other(
363 "Existing multipart Content-Type boundary is malformed or invalid",
364 ));
365 } else {
366 let value = existing.trim().trim_end_matches(';').trim_end();
367 let value = HeaderValue::from_str(&format!("{value}; boundary={boundary}"))
368 .expect("validated multipart boundary should build a valid Content-Type");
369 self.headers.insert(CONTENT_TYPE, value);
370 }
371 } else {
372 let value = HeaderValue::from_str(&format!("multipart/form-data; boundary={boundary}"))
373 .expect("validated multipart boundary should build a valid Content-Type");
374 self.headers.insert(CONTENT_TYPE, value);
375 }
376 self.body = HttpRequestBody::Multipart(body.into());
377 self.streaming_body = None;
378 Ok(self)
379 }
380
381 pub fn ndjson_body<T>(mut self, records: &[T]) -> HttpResult<Self>
392 where
393 T: Serialize,
394 {
395 let mut payload = String::new();
396 for record in records {
397 let line = serde_json::to_string(record)
398 .map_err(|error| HttpError::decode(format!("Failed to encode NDJSON record: {error}")))?;
399 payload.push_str(&line);
400 payload.push('\n');
401 }
402 if !self.headers.contains_key(CONTENT_TYPE) {
403 self.headers
404 .insert(CONTENT_TYPE, HeaderValue::from_static("application/x-ndjson"));
405 }
406 self.body = HttpRequestBody::Ndjson(Bytes::from(payload));
407 self.streaming_body = None;
408 Ok(self)
409 }
410
411 pub fn request_timeout(mut self, timeout: Duration) -> HttpResult<Self> {
425 validate_positive_timeout("request_timeout", timeout)?;
426 self.request_timeout = Some(timeout);
427 Ok(self)
428 }
429
430 pub fn write_timeout(mut self, timeout: Duration) -> HttpResult<Self> {
441 validate_positive_timeout("write_timeout", timeout)?;
442 self.write_timeout = timeout;
443 Ok(self)
444 }
445
446 pub fn read_timeout(mut self, timeout: Duration) -> HttpResult<Self> {
457 validate_positive_timeout("read_timeout", timeout)?;
458 self.read_timeout = timeout;
459 Ok(self)
460 }
461
462 pub fn base_url(mut self, base_url: Url) -> Self {
470 self.base_url = Some(base_url);
471 self
472 }
473
474 pub fn clear_base_url(mut self) -> Self {
479 self.base_url = None;
480 self
481 }
482
483 pub fn ipv4_only(mut self, enabled: bool) -> Self {
491 self.ipv4_only = enabled;
492 self
493 }
494
495 pub fn cancellation_token(mut self, token: CancellationToken) -> Self {
503 self.cancellation_token = Some(token);
504 self
505 }
506
507 pub fn force_retry(mut self) -> Self {
512 self.retry_override = self.retry_override.force_enable();
513 self
514 }
515
516 pub fn disable_retry(mut self) -> Self {
521 self.retry_override = self.retry_override.force_disable();
522 self
523 }
524
525 pub fn retry_method_policy(mut self, policy: HttpRetryMethodPolicy) -> Self {
533 self.retry_override = self.retry_override.with_method_policy(policy);
534 self
535 }
536
537 pub fn honor_retry_after(mut self, enabled: bool) -> Self {
546 self.retry_override = self.retry_override.with_honor_retry_after(enabled);
547 self
548 }
549
550 pub fn build(self) -> HttpRequest {
555 HttpRequest::new(self)
556 }
557
558 fn debug_resolved_url(&self) -> Option<Url> {
563 let mut url = match Url::parse(&self.path) {
564 Ok(url) => url,
565 Err(_) => self.base_url.as_ref()?.join(&self.path).ok()?,
566 };
567 if !self.query.is_empty() {
568 let mut pairs = url.query_pairs_mut();
569 for (key, value) in &self.query {
570 pairs.append_pair(key, value);
571 }
572 }
573 Some(url)
574 }
575}