1use bytes::Bytes;
61pub use http::{header, Method, StatusCode, Version};
62use http_body_util::{BodyExt, Full};
63use hyper::body::Incoming;
64use hyper::Request;
65use hyper_util::client::legacy::Client as HyperClient;
66use hyper_util::rt::TokioExecutor;
67use std::io::Read;
68use std::time::{Duration, Instant, SystemTime};
69use tracing::*;
70
71use crate::masking;
72
73#[cfg(all(feature = "native-tls", feature = "rustls-tls"))]
75compile_error!(
76 "Features `native-tls` and `rustls-tls` are mutually exclusive. Please enable only one."
77);
78
79#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
80compile_error!("Either feature `native-tls` or `rustls-tls` must be enabled for TLS support.");
81
82#[cfg(feature = "cookies")]
83use std::collections::HashMap;
84
85pub trait IntoUrl {
88 fn into_url_string(self) -> String;
89}
90
91impl IntoUrl for &str {
92 fn into_url_string(self) -> String {
93 self.to_string()
94 }
95}
96
97impl IntoUrl for String {
98 fn into_url_string(self) -> String {
99 self
100 }
101}
102
103impl IntoUrl for &String {
104 fn into_url_string(self) -> String {
105 self.clone()
106 }
107}
108
109impl IntoUrl for url::Url {
110 fn into_url_string(self) -> String {
111 self.to_string()
112 }
113}
114
115impl IntoUrl for &url::Url {
116 fn into_url_string(self) -> String {
117 self.to_string()
118 }
119}
120
121#[cfg(feature = "multipart")]
122#[derive(Debug)]
123pub struct MultipartForm {
124 }
127
128#[derive(Debug, thiserror::Error)]
129pub enum Error {
130 #[error("HttpError: {0}")]
131 Http(#[from] hyper::Error),
132 #[error("HttpError: {0}")]
133 HttpLegacy(#[from] hyper_util::client::legacy::Error),
134 #[error("UriError: {0}")]
135 Uri(#[from] http::uri::InvalidUri),
136 #[error("HeaderError: {0}")]
137 Header(#[from] http::Error),
138 #[cfg(feature = "native-tls")]
139 #[error("TlsError: {0}")]
140 Tls(#[from] hyper_tls::native_tls::Error),
141 #[cfg(feature = "rustls-tls")]
142 #[error("TlsError: {0}")]
143 Tls(#[from] rustls::Error),
144 #[error("Request timed out after {0:?}")]
145 Timeout(Duration),
146 #[error("failed to deserialize http response into the specified type: {0}")]
147 Deserialize(#[from] serde_json::Error),
148 #[error("{0:#}")]
149 Unexpected(#[from] eyre::Error),
150}
151
152#[derive(Debug, Clone)]
153pub struct LogRequest {
154 pub url: url::Url,
155 pub method: Method,
156 pub headers: header::HeaderMap,
157}
158
159#[derive(Debug, Clone, Default)]
160pub struct LogResponse {
161 pub headers: header::HeaderMap,
162 pub body: String,
163 pub status: StatusCode,
164 pub duration_req: Duration,
165}
166
167#[derive(Debug, Clone)]
168pub struct Log {
169 pub request: LogRequest,
170 pub response: LogResponse,
171 pub started_at: SystemTime,
172 pub ended_at: SystemTime,
173}
174
175#[derive(Debug, Clone)]
204pub struct Response {
205 pub headers: header::HeaderMap,
206 pub status: StatusCode,
207 pub text: String,
208 pub url: url::Url,
209 #[cfg(feature = "cookies")]
210 cookies: Vec<cookie::Cookie<'static>>,
211}
212
213impl Response {
214 pub fn status(&self) -> StatusCode {
224 self.status
225 }
226
227 pub fn headers(&self) -> &header::HeaderMap {
237 &self.headers
238 }
239
240 pub fn url(&self) -> &url::Url {
249 &self.url
250 }
251
252 pub async fn text(self) -> Result<String, Error> {
261 Ok(self.text)
262 }
263
264 pub async fn json<T: serde::de::DeserializeOwned>(self) -> Result<T, Error> {
280 Ok(serde_json::from_str(&self.text)?)
281 }
282
283 #[cfg(feature = "cookies")]
284 pub fn cookies(&self) -> impl Iterator<Item = &cookie::Cookie<'static>> + '_ {
285 self.cookies.iter()
286 }
287
288 async fn from(res: hyper::Response<Incoming>, url: url::Url) -> Result<Self, Error> {
289 let headers = res.headers().clone();
290 let status = res.status();
291
292 #[cfg(feature = "cookies")]
293 let cookies: Vec<cookie::Cookie<'static>> = headers
294 .get_all("set-cookie")
295 .iter()
296 .filter_map(|cookie_header| {
297 cookie_header.to_str().ok().and_then(|cookie_str| {
298 cookie::Cookie::parse(cookie_str)
299 .ok()
300 .map(|c| c.into_owned())
301 })
302 })
303 .collect();
304
305 let body_bytes = res.into_body().collect().await?.to_bytes();
306
307 let text = Self::decompress_body(&headers, &body_bytes);
309
310 Ok(Response {
311 headers,
312 status,
313 url,
314 text,
315 #[cfg(feature = "cookies")]
316 cookies,
317 })
318 }
319
320 fn decompress_body(headers: &header::HeaderMap, body_bytes: &Bytes) -> String {
321 match headers
322 .get("content-encoding")
323 .and_then(|v| v.to_str().ok())
324 {
325 Some("gzip") => {
326 use flate2::read::GzDecoder;
327 let mut decoder = GzDecoder::new(body_bytes.as_ref());
328 let mut decompressed = Vec::new();
329 match decoder.read_to_end(&mut decompressed) {
330 Ok(_) => String::from_utf8_lossy(&decompressed).to_string(),
331 Err(_) => String::from_utf8_lossy(body_bytes).to_string(),
332 }
333 }
334 Some("deflate") => {
335 use flate2::read::{DeflateDecoder, ZlibDecoder};
336
337 let mut zlib_decoder = ZlibDecoder::new(body_bytes.as_ref());
339 let mut decompressed = Vec::new();
340 match zlib_decoder.read_to_end(&mut decompressed) {
341 Ok(_) => String::from_utf8_lossy(&decompressed).to_string(),
342 Err(_) => {
343 let mut deflate_decoder = DeflateDecoder::new(body_bytes.as_ref());
345 let mut decompressed = Vec::new();
346 match deflate_decoder.read_to_end(&mut decompressed) {
347 Ok(_) => String::from_utf8_lossy(&decompressed).to_string(),
348 Err(_) => String::from_utf8_lossy(body_bytes).to_string(),
349 }
350 }
351 }
352 }
353 Some("br") => {
354 let mut decompressed = Vec::new();
355 match brotli::Decompressor::new(body_bytes.as_ref(), 4096)
356 .read_to_end(&mut decompressed)
357 {
358 Ok(_) => String::from_utf8_lossy(&decompressed).to_string(),
359 Err(_) => String::from_utf8_lossy(body_bytes).to_string(),
360 }
361 }
362 Some("zstd") => match zstd::decode_all(body_bytes.as_ref()) {
363 Ok(decompressed) => String::from_utf8_lossy(&decompressed).to_string(),
364 Err(_) => String::from_utf8_lossy(body_bytes).to_string(),
365 },
366 _ => String::from_utf8_lossy(body_bytes).to_string(),
367 }
368 }
369}
370
371#[derive(Clone)]
403pub struct Client {
404 #[cfg(feature = "native-tls")]
405 pub(crate) inner: HyperClient<
406 hyper_tls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>,
407 Full<Bytes>,
408 >,
409 #[cfg(feature = "rustls-tls")]
410 pub(crate) inner: HyperClient<
411 hyper_rustls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>,
412 Full<Bytes>,
413 >,
414 #[cfg(feature = "cookies")]
415 pub(crate) cookie_store:
416 std::sync::Arc<tokio::sync::RwLock<HashMap<String, Vec<cookie::Cookie<'static>>>>>,
417}
418
419impl Default for Client {
420 fn default() -> Self {
421 Self::new()
422 }
423}
424
425impl Client {
426 pub fn new() -> Client {
440 #[cfg(feature = "native-tls")]
441 let inner = {
442 let https = hyper_tls::HttpsConnector::new();
443 HyperClient::builder(TokioExecutor::new()).build::<_, Full<Bytes>>(https)
444 };
445
446 #[cfg(feature = "rustls-tls")]
447 let inner = {
448 let mut root_store = rustls::RootCertStore::empty();
449
450 #[cfg(feature = "rustls-tls-native-roots")]
451 {
452 let native_certs = rustls_native_certs::load_native_certs();
453 for cert in native_certs.certs {
454 root_store.add(cert).ok();
455 }
456 }
457
458 #[cfg(feature = "rustls-tls-webpki-roots")]
459 {
460 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
461 }
462
463 let tls_config = rustls::ClientConfig::builder()
464 .with_root_certificates(root_store)
465 .with_no_client_auth();
466
467 let https = hyper_rustls::HttpsConnectorBuilder::new()
468 .with_tls_config(tls_config)
469 .https_or_http()
470 .enable_http1()
471 .enable_http2()
472 .build();
473
474 HyperClient::builder(TokioExecutor::new()).build::<_, Full<Bytes>>(https)
475 };
476
477 Client {
478 inner,
479 #[cfg(feature = "cookies")]
480 cookie_store: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
481 }
482 }
483
484 pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
485 let url_str = url.into_url_string();
486 debug!("Requesting {url_str}");
487 RequestBuilder::new(self.clone(), Method::GET, &url_str)
488 }
489
490 pub fn post<U: IntoUrl>(&self, url: U) -> RequestBuilder {
491 let url_str = url.into_url_string();
492 debug!("Requesting {url_str}");
493 RequestBuilder::new(self.clone(), Method::POST, &url_str)
494 }
495
496 pub fn put<U: IntoUrl>(&self, url: U) -> RequestBuilder {
497 let url_str = url.into_url_string();
498 debug!("Requesting {url_str}");
499 RequestBuilder::new(self.clone(), Method::PUT, &url_str)
500 }
501
502 pub fn patch<U: IntoUrl>(&self, url: U) -> RequestBuilder {
503 let url_str = url.into_url_string();
504 debug!("Requesting {url_str}");
505 RequestBuilder::new(self.clone(), Method::PATCH, &url_str)
506 }
507
508 pub fn delete<U: IntoUrl>(&self, url: U) -> RequestBuilder {
509 let url_str = url.into_url_string();
510 debug!("Requesting {url_str}");
511 RequestBuilder::new(self.clone(), Method::DELETE, &url_str)
512 }
513
514 pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder {
515 let url_str = url.into_url_string();
516 debug!("Requesting {url_str}");
517 RequestBuilder::new(self.clone(), Method::HEAD, &url_str)
518 }
519
520 #[cfg(feature = "graphql")]
521 pub fn graphql<U: IntoUrl>(&self, url: U) -> crate::graphql::GraphqlRequestBuilder {
522 let url_str = url.into_url_string();
523 debug!("Requesting {url_str}");
524 crate::graphql::GraphqlRequestBuilder::new(RequestBuilder::new(
525 self.clone(),
526 Method::POST,
527 &url_str,
528 ))
529 }
530}
531
532pub struct RequestBuilder {
533 client: Client,
534 method: Method,
535 url: String,
536 headers: header::HeaderMap,
537 body: Option<Vec<u8>>,
538 query_params: Vec<(String, String)>,
539 timeout: Option<Duration>,
540}
541
542impl RequestBuilder {
543 fn new(client: Client, method: Method, url: &str) -> Self {
544 Self {
545 client,
546 method,
547 url: url.to_string(),
548 headers: header::HeaderMap::new(),
549 body: None,
550 query_params: Vec::new(),
551 timeout: None,
552 }
553 }
554
555 pub fn header<K, V>(mut self, key: K, value: V) -> Self
556 where
557 header::HeaderName: TryFrom<K>,
558 <header::HeaderName as TryFrom<K>>::Error: Into<http::Error>,
559 header::HeaderValue: TryFrom<V>,
560 <header::HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
561 {
562 if let (Ok(name), Ok(val)) = (
563 header::HeaderName::try_from(key),
564 header::HeaderValue::try_from(value),
565 ) {
566 self.headers.insert(name, val);
567 }
568 self
569 }
570
571 pub fn headers(mut self, headers: header::HeaderMap) -> Self {
572 self.headers.extend(headers);
573 self
574 }
575
576 pub fn basic_auth<U, P>(mut self, username: U, password: Option<P>) -> Self
577 where
578 U: std::fmt::Display,
579 P: std::fmt::Display,
580 {
581 let auth_value = match password {
582 Some(p) => format!("{username}:{p}"),
583 None => username.to_string(),
584 };
585 let encoded = base64::Engine::encode(
586 &base64::engine::general_purpose::STANDARD,
587 auth_value.as_bytes(),
588 );
589 let auth_header = format!("Basic {encoded}");
590
591 if let Ok(header_value) = header::HeaderValue::from_str(&auth_header) {
592 self.headers.insert(header::AUTHORIZATION, header_value);
593 }
594 self
595 }
596
597 pub fn bearer_auth<T>(mut self, token: T) -> Self
598 where
599 T: std::fmt::Display,
600 {
601 let auth_header = format!("Bearer {token}");
602 if let Ok(header_value) = header::HeaderValue::from_str(&auth_header) {
603 self.headers.insert(header::AUTHORIZATION, header_value);
604 }
605 self
606 }
607
608 pub fn body<T: Into<Vec<u8>>>(mut self, body: T) -> Self {
609 self.body = Some(body.into());
610 self
611 }
612
613 pub fn query<T: serde::Serialize + ?Sized>(mut self, query: &T) -> Self {
614 if let Ok(params) = serde_urlencoded::to_string(query) {
615 for pair in params.split('&') {
616 if let Some((key, value)) = pair.split_once('=') {
617 self.query_params.push((key.to_string(), value.to_string()));
618 }
619 }
620 }
621 self
622 }
623
624 pub fn form<T: serde::Serialize + ?Sized>(mut self, form: &T) -> Self {
625 if let Ok(body) = serde_urlencoded::to_string(form) {
626 self.body = Some(body.into_bytes());
627 self.headers.insert(
628 header::CONTENT_TYPE,
629 header::HeaderValue::from_static("application/x-www-form-urlencoded"),
630 );
631 }
632 self
633 }
634
635 #[cfg(feature = "json")]
636 pub fn json<T: serde::Serialize + ?Sized>(mut self, json: &T) -> Self {
637 if let Ok(body) = serde_json::to_string(json) {
638 self.body = Some(body.into_bytes());
639 self.headers.insert(
640 header::CONTENT_TYPE,
641 header::HeaderValue::from_static("application/json"),
642 );
643 }
644 self
645 }
646
647 #[cfg(feature = "multipart")]
648 pub fn multipart(self, _multipart: MultipartForm) -> Self {
649 self
652 }
653
654 pub async fn send(self) -> Result<Response, Error> {
655 let mut url = self.url.clone();
656
657 if !self.query_params.is_empty() {
659 let query_string: String = self
660 .query_params
661 .iter()
662 .map(|(k, v)| format!("{k}={v}"))
663 .collect::<Vec<_>>()
664 .join("&");
665
666 url = if url.contains('?') {
667 format!("{url}&{query_string}")
668 } else {
669 format!("{url}?{query_string}")
670 };
671 }
672
673 let parsed_url = url::Url::parse(&url).map_err(|e| eyre::eyre!("Invalid URL: {}", e))?;
674 let uri: http::Uri = url.parse()?;
675
676 let mut req_builder = Request::builder().method(self.method.clone()).uri(uri);
677
678 for (name, value) in &self.headers {
680 req_builder = req_builder.header(name, value);
681 }
682
683 #[cfg(feature = "cookies")]
684 {
685 let cookie_store = self.client.cookie_store.read().await;
687 if let Some(domain_cookies) = cookie_store.get(parsed_url.host_str().unwrap_or("")) {
688 if !domain_cookies.is_empty() {
689 let cookie_header = domain_cookies
690 .iter()
691 .map(|cookie| format!("{}={}", cookie.name(), cookie.value()))
692 .collect::<Vec<_>>()
693 .join("; ");
694
695 if let Ok(cookie_value) = header::HeaderValue::from_str(&cookie_header) {
696 req_builder = req_builder.header(header::COOKIE, cookie_value);
697 }
698 }
699 }
700 }
701
702 let body = match &self.body {
703 Some(ref body_data) => Full::new(Bytes::from(body_data.clone())),
704 None => Full::new(Bytes::new()),
705 };
706
707 let req = req_builder.body(body)?;
708
709 let log_request = LogRequest {
710 url: if masking::should_mask_sensitive() {
711 masking::mask_url(&parsed_url)
712 } else {
713 parsed_url.clone()
714 },
715 method: self.method.clone(),
716 headers: if masking::should_mask_sensitive() {
717 masking::mask_headers(&self.headers)
718 } else {
719 self.headers.clone()
720 },
721 };
722
723 let started_at = SystemTime::now();
724 let time_req = Instant::now();
725
726 let res = match self.timeout {
728 Some(timeout) => {
729 match tokio::time::timeout(timeout, self.client.inner.request(req)).await {
730 Ok(result) => result,
731 Err(_) => return Err(Error::Timeout(timeout)),
732 }
733 }
734 None => self.client.inner.request(req).await,
735 };
736 let ended_at = SystemTime::now();
737
738 match res {
739 Ok(res) => {
740 let status = res.status();
741
742 if status.is_redirection() {
744 return Self::follow_redirects(
745 self.client.clone(),
746 self.headers.clone(),
747 self.method.clone(),
748 self.body.clone(),
749 res,
750 parsed_url,
751 log_request,
752 started_at,
753 time_req,
754 10,
755 )
756 .await;
757 }
758
759 let response = Response::from(res, parsed_url).await?;
760 let duration_req = time_req.elapsed();
761
762 #[cfg(feature = "cookies")]
763 {
764 if !response.cookies.is_empty() {
766 let mut cookie_store = self.client.cookie_store.write().await;
767 let domain = response.url().host_str().unwrap_or("").to_string();
768 cookie_store.insert(domain, response.cookies.clone());
769 }
770 }
771
772 let log_response = LogResponse {
773 headers: if masking::should_mask_sensitive() {
774 masking::mask_headers(&response.headers)
775 } else {
776 response.headers.clone()
777 },
778 body: response.text.clone(),
779 status: response.status(),
780 duration_req,
781 };
782
783 crate::runner::publish(crate::runner::EventBody::Call(
784 crate::runner::CallLog::Http(Box::new(Log {
785 request: log_request,
786 response: log_response,
787 started_at,
788 ended_at,
789 })),
790 ))?;
791 Ok(response)
792 }
793 Err(e) => {
794 crate::runner::publish(crate::runner::EventBody::Call(
795 crate::runner::CallLog::Http(Box::new(Log {
796 request: log_request,
797 response: Default::default(),
798 started_at,
799 ended_at,
800 })),
801 ))?;
802 Err(e.into())
803 }
804 }
805 }
806
807 #[allow(clippy::too_many_arguments)]
808 async fn follow_redirects(
809 client: Client,
810 headers: header::HeaderMap,
811 mut method: Method,
812 body: Option<Vec<u8>>,
813 mut response: hyper::Response<Incoming>,
814 mut current_url: url::Url,
815 original_request: LogRequest,
816 started_at: SystemTime,
817 start_time: Instant,
818 max_redirects: u8,
819 ) -> Result<Response, Error> {
820 let mut redirect_count = 0;
821
822 loop {
823 let status = response.status();
824
825 if !status.is_redirection() || redirect_count >= max_redirects {
826 let ended_at = SystemTime::now();
827 let final_response = Response::from(response, current_url).await?;
828 let duration_req = start_time.elapsed();
829
830 #[cfg(feature = "cookies")]
831 {
832 if !final_response.cookies.is_empty() {
833 let mut cookie_store = client.cookie_store.write().await;
834 let domain = final_response.url().host_str().unwrap_or("").to_string();
835 cookie_store.insert(domain, final_response.cookies.clone());
836 }
837 }
838
839 let log_response = LogResponse {
840 headers: if masking::should_mask_sensitive() {
841 masking::mask_headers(&final_response.headers)
842 } else {
843 final_response.headers.clone()
844 },
845 body: final_response.text.clone(),
846 status: final_response.status(),
847 duration_req,
848 };
849
850 crate::runner::publish(crate::runner::EventBody::Call(
851 crate::runner::CallLog::Http(Box::new(Log {
852 request: original_request,
853 response: log_response,
854 started_at,
855 ended_at,
856 })),
857 ))?;
858
859 return Ok(final_response);
860 }
861
862 #[cfg(feature = "cookies")]
864 {
865 let redirect_cookies: Vec<cookie::Cookie<'static>> = response
866 .headers()
867 .get_all("set-cookie")
868 .iter()
869 .filter_map(|cookie_header| {
870 cookie_header.to_str().ok().and_then(|cookie_str| {
871 cookie::Cookie::parse(cookie_str)
872 .ok()
873 .map(|c| c.into_owned())
874 })
875 })
876 .collect();
877
878 if !redirect_cookies.is_empty() {
879 let mut cookie_store = client.cookie_store.write().await;
880 let domain = current_url.host_str().unwrap_or("").to_string();
881 let existing_cookies =
882 cookie_store.entry(domain.clone()).or_insert_with(Vec::new);
883 existing_cookies.extend(redirect_cookies);
884 }
885 }
886
887 let location = match response
889 .headers()
890 .get("location")
891 .and_then(|v| v.to_str().ok())
892 {
893 Some(loc) => loc,
894 None => {
895 let ended_at = SystemTime::now();
897 let final_response = Response::from(response, current_url).await?;
898 let duration_req = start_time.elapsed();
899
900 let log_response = LogResponse {
901 headers: if masking::should_mask_sensitive() {
902 masking::mask_headers(&final_response.headers)
903 } else {
904 final_response.headers.clone()
905 },
906 body: final_response.text.clone(),
907 status: final_response.status(),
908 duration_req,
909 };
910
911 crate::runner::publish(crate::runner::EventBody::Call(
912 crate::runner::CallLog::Http(Box::new(Log {
913 request: original_request,
914 response: log_response,
915 started_at,
916 ended_at,
917 })),
918 ))?;
919
920 return Ok(final_response);
921 }
922 };
923
924 current_url = if location.starts_with("http") {
926 url::Url::parse(location).map_err(|e| eyre::eyre!("Invalid redirect URL: {}", e))?
927 } else {
928 current_url
929 .join(location)
930 .map_err(|e| eyre::eyre!("Invalid redirect URL: {}", e))?
931 };
932
933 if status == StatusCode::SEE_OTHER
935 || (method == Method::POST
936 && (status == StatusCode::MOVED_PERMANENTLY || status == StatusCode::FOUND))
937 {
938 method = Method::GET;
939 }
940
941 let redirect_uri: http::Uri = current_url.to_string().parse()?;
943 let mut redirect_req_builder =
944 Request::builder().method(method.clone()).uri(redirect_uri);
945
946 for (name, value) in &headers {
948 redirect_req_builder = redirect_req_builder.header(name, value);
949 }
950
951 #[cfg(feature = "cookies")]
953 {
954 let cookie_store = client.cookie_store.read().await;
955 if let Some(domain_cookies) = cookie_store.get(current_url.host_str().unwrap_or(""))
956 {
957 if !domain_cookies.is_empty() {
958 let cookie_header = domain_cookies
959 .iter()
960 .map(|cookie| format!("{}={}", cookie.name(), cookie.value()))
961 .collect::<Vec<_>>()
962 .join("; ");
963
964 if let Ok(cookie_value) = header::HeaderValue::from_str(&cookie_header) {
965 redirect_req_builder =
966 redirect_req_builder.header(header::COOKIE, cookie_value);
967 }
968 }
969 }
970 }
971
972 let redirect_body = if method == Method::GET {
973 Full::new(Bytes::new())
974 } else {
975 match &body {
976 Some(body_data) => Full::new(Bytes::from(body_data.clone())),
977 None => Full::new(Bytes::new()),
978 }
979 };
980
981 let redirect_req = redirect_req_builder.body(redirect_body)?;
982 response = client.inner.request(redirect_req).await?;
983 redirect_count += 1;
984 }
985 }
986
987 pub fn timeout(mut self, timeout: Duration) -> Self {
988 self.timeout = Some(timeout);
989 self
990 }
991
992 pub fn try_clone(&self) -> Option<Self> {
993 Some(Self {
994 client: self.client.clone(),
995 method: self.method.clone(),
996 url: self.url.clone(),
997 headers: self.headers.clone(),
998 body: self.body.clone(),
999 query_params: self.query_params.clone(),
1000 timeout: self.timeout,
1001 })
1002 }
1003
1004 pub fn version(self, _version: Version) -> Self {
1005 self
1008 }
1009}