Skip to main content

qubit_http/request/
http_request.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9//! Immutable HTTP request object.
10
11use std::sync::RwLock;
12use std::time::Duration;
13
14use bytes::Bytes;
15use futures_util::stream as futures_stream;
16use http::{HeaderMap, HeaderName, HeaderValue, Method};
17use qubit_function::MutatingFunction;
18use reqwest::Response;
19use tokio_util::sync::CancellationToken;
20use url::Host;
21use url::Url;
22
23use crate::client::error_mapper::{map_reqwest_error, ReqwestErrorPhase};
24use crate::{AsyncHeaderInjector, HeaderInjector, HttpError, HttpErrorKind, HttpResult};
25
26use super::http_request_body::HttpRequestBody;
27use super::http_request_builder::HttpRequestBuilder;
28use super::http_request_retry_override::HttpRequestRetryOverride;
29use super::parse_header;
30
31/// Immutable snapshot of a single HTTP call produced by
32/// [`crate::HttpRequestBuilder`].
33#[derive(Debug)]
34pub struct HttpRequest {
35    /// HTTP method (GET, POST, …).
36    method: Method,
37    /// Absolute URL string, or path joined with client `base_url` when not
38    /// parseable as URL.
39    path: String,
40    /// Query string parameters as `(name, value)` pairs.
41    query: Vec<(String, String)>,
42    /// Headers added on top of client defaults and injector output.
43    headers: HeaderMap,
44    /// Serialized body variant.
45    body: HttpRequestBody,
46    /// Overrides client-wide request timeout when set; otherwise client default
47    /// applies.
48    request_timeout: Option<Duration>,
49    /// Per-request write timeout used during request sending.
50    write_timeout: Duration,
51    /// Per-request read timeout used during response body reads.
52    read_timeout: Duration,
53    /// Base URL copied from client options, used to resolve relative `path`.
54    base_url: Option<Url>,
55    /// Lazily maintained cache for the currently resolved URL.
56    resolved_url: RwLock<Option<Url>>,
57    /// Attempt-scoped cache of merged outbound headers after applying
58    /// defaults/injectors/request-local headers.
59    effective_headers: Option<HeaderMap>,
60    /// Whether resolved URLs must avoid IPv6 literal hosts.
61    ipv4_only: bool,
62    /// Optional cancellation token checked before send and during I/O phases.
63    cancellation_token: Option<CancellationToken>,
64    /// Per-request retry override (enable/disable/method-policy/Retry-After
65    /// behavior).
66    retry_override: HttpRequestRetryOverride,
67    /// Client default headers snapshot captured when this request builder was
68    /// created.
69    default_headers: HeaderMap,
70    /// Client sync header injectors snapshot captured when this request builder
71    /// was created.
72    injectors: Vec<HeaderInjector>,
73    /// Client async header injectors snapshot captured when this request
74    /// builder was created.
75    async_injectors: Vec<AsyncHeaderInjector>,
76}
77
78impl HttpRequest {
79    /// Consumes a finished [`HttpRequestBuilder`] and freezes its fields into
80    /// an [`HttpRequest`].
81    ///
82    /// # Parameters
83    /// - `builder`: Populated builder produced by the HTTP client pipeline.
84    ///
85    /// # Returns
86    /// Snapshot ready for URL resolution, header assembly, and sending.
87    pub(super) fn new(builder: HttpRequestBuilder) -> Self {
88        let mut request = Self {
89            method: builder.method,
90            path: builder.path,
91            query: builder.query,
92            headers: builder.headers,
93            body: builder.body,
94            request_timeout: builder.request_timeout,
95            write_timeout: builder.write_timeout,
96            read_timeout: builder.read_timeout,
97            base_url: builder.base_url,
98            resolved_url: RwLock::new(None),
99            effective_headers: None,
100            ipv4_only: builder.ipv4_only,
101            cancellation_token: builder.cancellation_token,
102            retry_override: builder.retry_override,
103            default_headers: builder.default_headers,
104            injectors: builder.injectors,
105            async_injectors: builder.async_injectors,
106        };
107        request.refresh_resolved_url_cache();
108        request
109    }
110
111    /// Returns the HTTP verb for this snapshot.
112    ///
113    /// # Returns
114    /// Borrowed [`Method`] (for example GET or POST).
115    pub fn method(&self) -> &Method {
116        &self.method
117    }
118
119    /// Replaces the HTTP verb.
120    ///
121    /// # Parameters
122    /// - `method`: New [`Method`].
123    ///
124    /// # Returns
125    /// `self` for method chaining.
126    pub fn set_method(&mut self, method: Method) -> &mut Self {
127        self.method = method;
128        self
129    }
130
131    /// Returns the path segment or absolute URL string stored on this request.
132    ///
133    /// # Returns
134    /// The raw path/URL before query string assembly; may be relative if a base
135    /// URL is set.
136    pub fn path(&self) -> &str {
137        &self.path
138    }
139
140    /// Replaces the path or absolute URL string.
141    ///
142    /// # Parameters
143    /// - `path`: New path or URL string (query string is managed separately via
144    ///   [`Self::add_query_param`]).
145    ///
146    /// # Returns
147    /// `self` for method chaining.
148    pub fn set_path(&mut self, path: impl Into<String>) -> &mut Self {
149        self.path = path.into();
150        self.refresh_resolved_url_cache();
151        self
152    }
153
154    /// Returns ordered `(name, value)` query pairs that will be appended to the
155    /// resolved URL.
156    ///
157    /// # Returns
158    /// Slice view of accumulated query parameters.
159    pub fn query(&self) -> &[(String, String)] {
160        &self.query
161    }
162
163    /// Appends a single query pair preserving insertion order.
164    ///
165    /// # Parameters
166    /// - `key`: Parameter name.
167    /// - `value`: Parameter value.
168    ///
169    /// # Returns
170    /// `self` for method chaining.
171    pub fn add_query_param(
172        &mut self,
173        key: impl Into<String>,
174        value: impl Into<String>,
175    ) -> &mut Self {
176        self.query.push((key.into(), value.into()));
177        self
178    }
179
180    /// Removes every query pair from this snapshot.
181    ///
182    /// # Returns
183    /// `self` for method chaining.
184    pub fn clear_query_params(&mut self) -> &mut Self {
185        self.query.clear();
186        self
187    }
188
189    /// Returns request-local headers layered on top of client defaults and
190    /// injector output at send time.
191    ///
192    /// # Returns
193    /// Borrowed [`HeaderMap`] owned by this request only (not merged defaults).
194    pub fn headers(&self) -> &HeaderMap {
195        &self.headers
196    }
197
198    /// Parses and inserts one header from string name/value pairs.
199    ///
200    /// # Parameters
201    /// - `name`: Header field name.
202    /// - `value`: Header field value.
203    ///
204    /// # Returns
205    /// `Ok(self)` on success.
206    ///
207    /// # Errors
208    /// Returns [`HttpError`] when name or value cannot be converted into valid
209    /// HTTP tokens.
210    pub fn set_header(&mut self, name: &str, value: &str) -> Result<&mut Self, HttpError> {
211        let (header_name, header_value) = parse_header(name, value)?;
212        self.headers.insert(header_name, header_value);
213        self.invalidate_effective_headers_cache();
214        Ok(self)
215    }
216
217    /// Inserts one header using pre-validated [`HeaderName`] / [`HeaderValue`]
218    /// types.
219    ///
220    /// # Parameters
221    /// - `name`: Typed header name.
222    /// - `value`: Typed header value.
223    ///
224    /// # Returns
225    /// `self` for method chaining.
226    pub fn set_typed_header(&mut self, name: HeaderName, value: HeaderValue) -> &mut Self {
227        self.headers.insert(name, value);
228        self.invalidate_effective_headers_cache();
229        self
230    }
231
232    /// Removes all values for a header field by typed name.
233    ///
234    /// # Parameters
235    /// - `name`: Header name to strip from the request-local map.
236    ///
237    /// # Returns
238    /// `self` for method chaining.
239    pub fn remove_header(&mut self, name: &HeaderName) -> &mut Self {
240        self.headers.remove(name);
241        self.invalidate_effective_headers_cache();
242        self
243    }
244
245    /// Clears all request-local headers (defaults and injectors are unaffected
246    /// until send).
247    ///
248    /// # Returns
249    /// `self` for method chaining.
250    pub fn clear_headers(&mut self) -> &mut Self {
251        self.headers.clear();
252        self.invalidate_effective_headers_cache();
253        self
254    }
255
256    /// Returns the serialized body variant for this snapshot.
257    ///
258    /// # Returns
259    /// Borrowed [`HttpRequestBody`].
260    pub fn body(&self) -> &HttpRequestBody {
261        &self.body
262    }
263
264    /// Replaces the entire body payload.
265    ///
266    /// # Parameters
267    /// - `body`: New [`HttpRequestBody`] variant.
268    ///
269    /// # Returns
270    /// `self` for method chaining.
271    pub fn set_body(&mut self, body: HttpRequestBody) -> &mut Self {
272        self.body = body;
273        self
274    }
275
276    /// Returns the per-request total timeout, if any.
277    ///
278    /// # Returns
279    /// `Some(duration)` when a request-specific timeout overrides the client
280    /// default; otherwise `None`.
281    pub fn request_timeout(&self) -> Option<Duration> {
282        self.request_timeout
283    }
284
285    /// Sets a per-request total timeout that overrides the client default for
286    /// this send.
287    ///
288    /// # Parameters
289    /// - `timeout`: Upper bound for the entire request lifecycle handled by
290    ///   reqwest.
291    ///
292    /// # Returns
293    /// `self` for method chaining.
294    pub fn set_request_timeout(&mut self, timeout: Duration) -> &mut Self {
295        self.request_timeout = Some(timeout);
296        self
297    }
298
299    /// Drops the per-request timeout so the client-wide default applies again.
300    ///
301    /// # Returns
302    /// `self` for method chaining.
303    pub fn clear_request_timeout(&mut self) -> &mut Self {
304        self.request_timeout = None;
305        self
306    }
307
308    /// Returns the write-phase timeout used while sending the request.
309    pub fn write_timeout(&self) -> Duration {
310        self.write_timeout
311    }
312
313    /// Sets the write-phase timeout used while sending the request.
314    pub fn set_write_timeout(&mut self, timeout: Duration) -> &mut Self {
315        self.write_timeout = timeout;
316        self
317    }
318
319    /// Returns the read-phase timeout used while reading response body bytes.
320    pub fn read_timeout(&self) -> Duration {
321        self.read_timeout
322    }
323
324    /// Sets the read-phase timeout used while reading response body bytes.
325    pub fn set_read_timeout(&mut self, timeout: Duration) -> &mut Self {
326        self.read_timeout = timeout;
327        self
328    }
329
330    /// Returns the optional base URL used to resolve relative [`Self::path`]
331    /// values.
332    ///
333    /// # Returns
334    /// `Some` when a base is configured; `None` when only absolute URLs in
335    /// `path` are valid.
336    pub fn base_url(&self) -> Option<&Url> {
337        self.base_url.as_ref()
338    }
339
340    /// Sets the base URL used by [`Self::resolved_url`] when `path` is not
341    /// absolute.
342    ///
343    /// # Parameters
344    /// - `base_url`: Root URL to join against relative paths.
345    ///
346    /// # Returns
347    /// `self` for method chaining.
348    pub fn set_base_url(&mut self, base_url: Url) -> &mut Self {
349        self.base_url = Some(base_url);
350        self.refresh_resolved_url_cache();
351        self
352    }
353
354    /// Removes the configured base URL so relative paths can no longer be
355    /// resolved without resetting it.
356    ///
357    /// # Returns
358    /// `self` for method chaining.
359    pub fn clear_base_url(&mut self) -> &mut Self {
360        self.base_url = None;
361        self.refresh_resolved_url_cache();
362        self
363    }
364
365    /// Returns whether IPv6 literal hosts are rejected after URL resolution.
366    ///
367    /// # Returns
368    /// `true` when a resolved URL whose host is an IPv6 literal must be
369    /// rejected with [`HttpError::invalid_url`].
370    pub fn ipv4_only(&self) -> bool {
371        self.ipv4_only
372    }
373
374    /// Enables or disables IPv6 literal host rejection for resolved URLs.
375    ///
376    /// # Parameters
377    /// - `enabled`: When `true`, resolved URLs whose host is an IPv6 literal
378    ///   are errors.
379    ///
380    /// # Returns
381    /// `self` for method chaining.
382    pub fn set_ipv4_only(&mut self, enabled: bool) -> &mut Self {
383        self.ipv4_only = enabled;
384        self.refresh_resolved_url_cache();
385        self
386    }
387
388    /// Returns the cooperative cancellation handle, if configured.
389    ///
390    /// # Returns
391    /// `Some` token checked before send and during I/O; `None` when
392    /// cancellation is not wired.
393    pub fn cancellation_token(&self) -> Option<&CancellationToken> {
394        self.cancellation_token.as_ref()
395    }
396
397    /// Attaches a [`CancellationToken`] that can abort this request
398    /// cooperatively.
399    ///
400    /// # Parameters
401    /// - `token`: Shared cancellation source.
402    ///
403    /// # Returns
404    /// `self` for method chaining.
405    pub fn set_cancellation_token(&mut self, token: CancellationToken) -> &mut Self {
406        self.cancellation_token = Some(token);
407        self
408    }
409
410    /// Removes any cancellation token from this snapshot.
411    ///
412    /// # Returns
413    /// `self` for method chaining.
414    pub fn clear_cancellation_token(&mut self) -> &mut Self {
415        self.cancellation_token = None;
416        self
417    }
418
419    /// Returns the per-request retry override applied by the client pipeline.
420    ///
421    /// # Returns
422    /// Borrowed [`HttpRequestRetryOverride`].
423    pub fn retry_override(&self) -> &HttpRequestRetryOverride {
424        &self.retry_override
425    }
426
427    /// Replaces the retry override for this single request.
428    ///
429    /// # Parameters
430    /// - `retry_override`: New override policy and knobs.
431    ///
432    /// # Returns
433    /// `self` for method chaining.
434    pub fn set_retry_override(&mut self, retry_override: HttpRequestRetryOverride) -> &mut Self {
435        self.retry_override = retry_override;
436        self
437    }
438
439    /// Moves the current body out, leaving [`HttpRequestBody::Empty`] in its
440    /// place.
441    ///
442    /// Used internally before handing the payload to reqwest so the snapshot is
443    /// not cloned twice.
444    ///
445    /// # Returns
446    /// Previous [`HttpRequestBody`] value.
447    pub(crate) fn take_body(&mut self) -> HttpRequestBody {
448        std::mem::replace(&mut self.body, HttpRequestBody::Empty)
449    }
450
451    /// Assembles a reqwest [`RequestBuilder`](reqwest::RequestBuilder), applies
452    /// this snapshot's body, then sends with a bounded write phase.
453    ///
454    /// Centralizes query/timeout/body wiring plus cooperative cancellation and
455    /// write-timeout handling; higher-level retry, logging, and interceptors
456    /// stay in [`crate::HttpClient`].
457    ///
458    /// # Parameters
459    /// - `backend`: Shared reqwest client.
460    ///
461    /// # Returns
462    /// The successful [`Response`] or a mapped [`HttpError`].
463    ///
464    /// # Errors
465    /// - Cooperative cancellation while waiting on the send future.
466    /// - Transport failures mapped from reqwest.
467    /// - Write timeout when the send future does not complete within
468    ///   `write_timeout`.
469    pub(crate) async fn send_impl(&mut self, backend: &reqwest::Client) -> HttpResult<Response> {
470        let method = self.method.clone();
471        let url = self.resolved_url()?;
472        let headers = self.effective_headers().await?.clone();
473        let mut builder = backend.request(method.clone(), url.clone());
474        builder = builder.headers(headers);
475        if !self.query.is_empty() {
476            builder = builder.query(self.query.as_slice());
477        }
478        if let Some(timeout) = self.request_timeout {
479            builder = builder.timeout(timeout);
480        }
481        builder = Self::apply_request_body(builder, self.take_body());
482
483        let send_future = tokio::time::timeout(self.write_timeout, builder.send());
484        let next = if let Some(token) = self.cancellation_token.as_ref() {
485            tokio::select! {
486                _ = token.cancelled() => {
487                    return Err(HttpError::cancelled("Request cancelled while sending")
488                        .with_method(&method)
489                        .with_url(&url));
490                }
491                send_result = send_future => send_result,
492            }
493        } else {
494            send_future.await
495        };
496
497        match next {
498            Ok(Ok(response)) => Ok(response),
499            Ok(Err(error)) => Err(map_reqwest_error(
500                error,
501                HttpErrorKind::Transport,
502                Some(ReqwestErrorPhase::Send),
503                Some(method.clone()),
504                Some(url.clone()),
505            )),
506            Err(_) => Err(HttpError::write_timeout(format!(
507                "Write timeout after {:?} while sending request",
508                self.write_timeout
509            ))
510            .with_method(&method)
511            .with_url(&url)),
512        }
513    }
514
515    /// Returns the resolved URL for current request fields, computing and
516    /// caching it on demand.
517    ///
518    /// # Returns
519    /// Resolved [`Url`] value (cloned from cache when already computed).
520    ///
521    /// # Errors
522    /// Returns [`HttpError::invalid_url`] when parsing fails, the base URL is
523    /// missing for a relative path, joining fails, or [`Self::ipv4_only`]
524    /// rejects an IPv6 literal host.
525    pub(crate) fn resolved_url(&self) -> Result<Url, HttpError> {
526        if let Some(url) = self
527            .resolved_url
528            .read()
529            .expect("resolved_url read lock poisoned")
530            .as_ref()
531        {
532            return Ok(url.clone());
533        }
534        let resolved = self.compute_resolved_url()?;
535        *self
536            .resolved_url
537            .write()
538            .expect("resolved_url write lock poisoned") = Some(resolved.clone());
539        Ok(resolved)
540    }
541
542    /// Returns cached resolved URL when available.
543    pub fn resolved_url_cached(&self) -> Option<Url> {
544        self.resolved_url
545            .read()
546            .expect("resolved_url read lock poisoned")
547            .clone()
548    }
549
550    /// Recomputes and stores the current resolved URL.
551    fn refresh_resolved_url_cache(&mut self) {
552        *self
553            .resolved_url
554            .write()
555            .expect("resolved_url write lock poisoned") = self.compute_resolved_url().ok();
556    }
557
558    /// Computes the resolved URL from current path/base/ipv4 settings.
559    fn compute_resolved_url(&self) -> Result<Url, HttpError> {
560        if let Ok(url) = Url::parse(&self.path) {
561            self.validate_resolved_url_host(&url)?;
562            return Ok(url);
563        }
564
565        let base = self.base_url.as_ref().ok_or_else(|| {
566            HttpError::invalid_url(format!(
567                "Cannot resolve relative path '{}' without base_url",
568                self.path
569            ))
570        })?;
571
572        let url = base.join(&self.path).map_err(|error| {
573            HttpError::invalid_url(format!(
574                "Failed to resolve path '{}' against base URL '{}': {}",
575                self.path, base, error
576            ))
577        })?;
578        self.validate_resolved_url_host(&url)?;
579        Ok(url)
580    }
581
582    /// Enforces [`Self::ipv4_only`] by rejecting IPv6 literal hosts in `url`.
583    ///
584    /// # Parameters
585    /// - `url`: Candidate URL after parsing or joining.
586    ///
587    /// # Returns
588    /// `Ok(())` when the host is acceptable.
589    ///
590    /// # Errors
591    /// [`HttpError::invalid_url`] when `ipv4_only` is `true` and the host is an
592    /// IPv6 literal.
593    fn validate_resolved_url_host(&self, url: &Url) -> Result<(), HttpError> {
594        if self.ipv4_only && matches!(url.host(), Some(Host::Ipv6(_))) {
595            return Err(HttpError::invalid_url(format!(
596                "IPv6 literal host is not allowed when ipv4_only=true: {}",
597                url
598            )));
599        }
600        Ok(())
601    }
602
603    /// Returns the attempt-scoped merged outbound headers.
604    ///
605    /// On first call after invalidation, this computes merged headers by
606    /// replaying defaults/injectors/request-local headers and stores them in
607    /// [`Self::effective_headers`]. Later calls in the same attempt return the
608    /// cached map.
609    ///
610    /// Merge order (later wins on duplicates):
611    /// 1. Client default headers snapshot captured when the builder was
612    ///    created.
613    /// 2. Synchronous injector output in registration order.
614    /// 3. Asynchronous injector output in registration order.
615    /// 4. Request-local headers from this snapshot.
616    ///
617    /// # Returns
618    /// Borrowed merged [`HeaderMap`] from the cache.
619    ///
620    /// # Errors
621    /// Propagates failures returned by any injector's `apply` implementation.
622    pub(crate) async fn effective_headers(&mut self) -> HttpResult<&HeaderMap> {
623        if self.effective_headers.is_none() {
624            self.effective_headers = Some(self.compute_effective_headers().await?);
625        }
626        Ok(self
627            .effective_headers
628            .as_ref()
629            .expect("effective_headers must exist after successful effective_headers"))
630    }
631
632    /// Returns cached merged outbound headers when available.
633    pub(crate) fn effective_headers_cached(&self) -> Option<&HeaderMap> {
634        self.effective_headers.as_ref()
635    }
636
637    /// Clears the effective-header cache.
638    ///
639    /// This method invalidates [`Self::effective_headers`] so the next call to
640    /// [`Self::effective_headers`] recomputes merged headers by re-running
641    /// defaults and header injectors.
642    ///
643    /// Why this is needed:
644    /// - request-local headers may have been mutated (`set_header`, `clear_headers`, etc.);
645    /// - injector output may be time-sensitive (for example rotating auth token
646    ///   or timestamp-based signatures), so each send attempt should recompute
647    ///   merged headers instead of reusing stale values from prior attempts.
648    ///
649    /// When to call:
650    /// - immediately before starting a new send attempt;
651    /// - after any mutation that can change final outbound headers.
652    pub(crate) fn invalidate_effective_headers_cache(&mut self) {
653        self.effective_headers = None;
654    }
655
656    /// Computes merged outbound headers without touching the cache.
657    async fn compute_effective_headers(&self) -> HttpResult<HeaderMap> {
658        let mut headers = self.default_headers.clone();
659
660        for injector in &self.injectors {
661            injector.apply(&mut headers)?;
662        }
663        for injector in &self.async_injectors {
664            injector.apply(&mut headers).await?;
665        }
666
667        headers.extend(self.headers.clone());
668        Ok(headers)
669    }
670
671    /// Returns a pre-cancelled [`HttpError`] when a token is present and
672    /// already cancelled.
673    ///
674    /// # Parameters
675    /// - `message`: Human-readable cancellation reason.
676    ///
677    /// # Returns
678    /// `Some` [`HttpError`] (including method context and cached URL when
679    /// available) when a token exists and is already cancelled; otherwise
680    /// `None`.
681    pub(crate) fn cancelled_error_if_needed(&self, message: &str) -> Option<HttpError> {
682        if self
683            .cancellation_token
684            .as_ref()
685            .is_some_and(CancellationToken::is_cancelled)
686        {
687            let mut error = HttpError::cancelled(message.to_string()).with_method(&self.method);
688            if let Ok(url) = self.resolved_url() {
689                error = error.with_url(&url);
690            }
691            Some(error)
692        } else {
693            None
694        }
695    }
696
697    /// Attaches the correct reqwest body encoding for each [`HttpRequestBody`]
698    /// variant.
699    ///
700    /// # Parameters
701    /// - `builder`: Partially configured [`reqwest::RequestBuilder`]
702    ///   (method/URL/headers already set).
703    /// - `body`: Payload variant to attach; moved into the builder.
704    ///
705    /// # Returns
706    /// The same builder with an appropriate `.body(...)` applied (or unchanged
707    /// for [`HttpRequestBody::Empty`]).
708    fn apply_request_body(
709        builder: reqwest::RequestBuilder,
710        body: HttpRequestBody,
711    ) -> reqwest::RequestBuilder {
712        match body {
713            HttpRequestBody::Empty => builder,
714            HttpRequestBody::Bytes(bytes)
715            | HttpRequestBody::Json(bytes)
716            | HttpRequestBody::Form(bytes)
717            | HttpRequestBody::Multipart(bytes)
718            | HttpRequestBody::Ndjson(bytes) => builder.body(bytes),
719            HttpRequestBody::Stream(chunks) => {
720                let body_stream = futures_stream::iter(
721                    chunks.into_iter().map(Result::<Bytes, std::io::Error>::Ok),
722                );
723                builder.body(reqwest::Body::wrap_stream(body_stream))
724            }
725            HttpRequestBody::Text(text) => builder.body(text),
726        }
727    }
728}
729
730impl Clone for HttpRequest {
731    fn clone(&self) -> Self {
732        Self {
733            method: self.method.clone(),
734            path: self.path.clone(),
735            query: self.query.clone(),
736            headers: self.headers.clone(),
737            body: self.body.clone(),
738            request_timeout: self.request_timeout,
739            write_timeout: self.write_timeout,
740            read_timeout: self.read_timeout,
741            base_url: self.base_url.clone(),
742            resolved_url: RwLock::new(self.resolved_url_cached()),
743            effective_headers: self.effective_headers.clone(),
744            ipv4_only: self.ipv4_only,
745            cancellation_token: self.cancellation_token.clone(),
746            retry_override: self.retry_override.clone(),
747            default_headers: self.default_headers.clone(),
748            injectors: self.injectors.clone(),
749            async_injectors: self.async_injectors.clone(),
750        }
751    }
752}