1#![forbid(unsafe_code)]
20
21pub mod client_builder;
22pub mod middleware;
23pub mod proxy;
24pub mod redirect;
25pub mod resolver;
26pub mod retry;
27
28#[cfg(feature = "tls")]
29pub mod connector;
30#[cfg(feature = "tls")]
31pub mod request_config;
32#[cfg(feature = "tls")]
33pub mod tls;
34
35#[cfg(feature = "h3")]
36pub mod h3;
37
38#[cfg(feature = "tls")]
39pub use connector::{MaybeHttpsStream, OxiHttpsConnector};
40#[cfg(feature = "tls")]
41pub use request_config::RequestTlsConfig;
42#[cfg(feature = "tls")]
43pub use tls::DangerousNoVerification;
44
45#[cfg(feature = "socks")]
46pub use proxy::Socks5Connector;
47pub use proxy::{ProxyConnector, ProxyKind};
48
49use bytes::Bytes;
50use futures_core::Stream;
51use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri};
52use http_body_util::{BodyExt, Full};
53use hyper::body::Incoming;
54use hyper_util::client::legacy::connect::{Connect, HttpConnector};
55use hyper_util::client::legacy::Client as HyperClient;
56#[cfg(feature = "tls")]
57use hyper_util::rt::TokioExecutor;
58use resolver::BoxResolver;
59use std::pin::Pin;
60use std::str::FromStr;
61use std::sync::Arc;
62use std::task::{Context, Poll};
63use std::time::{Duration, Instant};
64
65#[cfg(feature = "tls")]
66pub(crate) use client_builder::apply_http2_settings;
67pub use client_builder::{ClientBuilder, Http2Settings};
68pub use middleware::{ClientMiddleware, LoggingMiddleware, TimingMiddleware};
69use oxihttp_core::OxiHttpError;
70pub use redirect::RedirectPolicy;
71pub use retry::RetryPolicy;
72
73pub struct BodyStream {
79 inner: http_body_util::BodyStream<Incoming>,
80}
81
82impl Stream for BodyStream {
83 type Item = Result<Bytes, OxiHttpError>;
84
85 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
86 loop {
87 match Pin::new(&mut self.inner).poll_next(cx) {
88 Poll::Ready(Some(Ok(frame))) => {
89 if let Ok(data) = frame.into_data() {
90 return Poll::Ready(Some(Ok(data)));
91 }
92 }
94 Poll::Ready(Some(Err(e))) => {
95 return Poll::Ready(Some(Err(OxiHttpError::Body(e.to_string()))));
96 }
97 Poll::Ready(None) => return Poll::Ready(None),
98 Poll::Pending => return Poll::Pending,
99 }
100 }
101 }
102}
103
104pub struct Response {
110 inner: http::Response<Incoming>,
111 decompress: bool,
113}
114
115impl Response {
116 pub fn status(&self) -> StatusCode {
118 self.inner.status()
119 }
120
121 pub fn headers(&self) -> &HeaderMap {
123 self.inner.headers()
124 }
125
126 pub fn header(&self, name: &str) -> Option<&str> {
147 self.inner.headers().get(name).and_then(|v| v.to_str().ok())
148 }
149
150 pub fn version(&self) -> http::Version {
152 self.inner.version()
153 }
154
155 pub fn content_length(&self) -> Option<u64> {
157 self.inner
158 .headers()
159 .get(http::header::CONTENT_LENGTH)
160 .and_then(|v| v.to_str().ok())
161 .and_then(|s| s.parse().ok())
162 }
163
164 pub async fn body_bytes(self) -> Result<Bytes, OxiHttpError> {
166 let decompress = self.decompress;
167 let ce = self
168 .inner
169 .headers()
170 .get(http::header::CONTENT_ENCODING)
171 .and_then(|v| v.to_str().ok())
172 .map(|s| s.to_ascii_lowercase());
173
174 let raw = self
175 .inner
176 .into_body()
177 .collect()
178 .await
179 .map(|c| c.to_bytes())
180 .map_err(|e| OxiHttpError::Body(e.to_string()))?;
181
182 if decompress {
183 match ce.as_deref() {
184 Some("gzip") => {
185 #[cfg(feature = "decompression")]
186 {
187 let decompressed = oxiarc_deflate::gzip_decompress(&raw).map_err(|e| {
188 OxiHttpError::Body(format!("gzip decompression error: {e}"))
189 })?;
190 return Ok(Bytes::from(decompressed));
191 }
192 #[cfg(not(feature = "decompression"))]
193 {
194 }
196 }
197 Some("deflate") => {
198 #[cfg(feature = "decompression")]
199 {
200 let decompressed = oxiarc_deflate::zlib_decompress(&raw)
201 .or_else(|_| {
202 oxiarc_deflate::inflate(&raw).map_err(|e| {
204 OxiHttpError::Body(format!("deflate decompression error: {e}"))
205 })
206 })
207 .map_err(|e| {
208 OxiHttpError::Body(format!("deflate decompression error: {e}"))
209 })?;
210 return Ok(Bytes::from(decompressed));
211 }
212 #[cfg(not(feature = "decompression"))]
213 {
214 }
216 }
217 _ => {}
218 }
219 }
220
221 Ok(raw)
222 }
223
224 pub async fn body_text(self) -> Result<String, OxiHttpError> {
226 let bytes = self.body_bytes().await?;
227 String::from_utf8(bytes.to_vec())
228 .map_err(|e| OxiHttpError::Body(format!("invalid UTF-8: {e}")))
229 }
230
231 pub async fn body_json<T: serde::de::DeserializeOwned>(self) -> Result<T, OxiHttpError> {
233 let bytes = self.body_bytes().await?;
234 serde_json::from_slice(&bytes).map_err(|e| OxiHttpError::Json(e.to_string()))
235 }
236
237 pub fn error_for_status(self) -> Result<Self, OxiHttpError> {
241 let status = self.inner.status();
242 if status.is_client_error() || status.is_server_error() {
243 Err(OxiHttpError::Body(format!(
244 "HTTP error: {} {}",
245 status.as_u16(),
246 status.canonical_reason().unwrap_or("Unknown")
247 )))
248 } else {
249 Ok(self)
250 }
251 }
252
253 pub fn content_type(&self) -> Option<&str> {
255 self.inner
256 .headers()
257 .get(http::header::CONTENT_TYPE)
258 .and_then(|v| v.to_str().ok())
259 }
260
261 pub fn cookies(&self) -> Vec<oxihttp_core::Cookie> {
266 self.inner
267 .headers()
268 .get_all(http::header::SET_COOKIE)
269 .iter()
270 .filter_map(|v| v.to_str().ok())
271 .filter_map(oxihttp_core::Cookie::parse_set_cookie)
272 .collect()
273 }
274
275 pub fn body_stream(self) -> BodyStream {
277 BodyStream {
278 inner: http_body_util::BodyStream::new(self.inner.into_body()),
279 }
280 }
281}
282
283impl std::fmt::Debug for Response {
284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285 f.debug_struct("Response")
286 .field("status", &self.inner.status())
287 .field("version", &self.inner.version())
288 .field("headers", self.inner.headers())
289 .finish()
290 }
291}
292
293pub struct RequestBuilder<C = HttpConnector> {
301 client: HyperClient<C, Full<Bytes>>,
302 method: Method,
303 uri: Uri,
304 headers: HeaderMap,
305 body: Bytes,
306 timeout: Option<Duration>,
307 redirect_policy: RedirectPolicy,
308 retry_policy: Option<RetryPolicy>,
309 decompression: bool,
310 middleware: Vec<Arc<dyn ClientMiddleware>>,
311 cookie_jar: Option<Arc<std::sync::Mutex<oxihttp_core::CookieJar>>>,
312}
313
314impl<C> RequestBuilder<C>
315where
316 C: Connect + Clone + Send + Sync + 'static,
317{
318 #[allow(clippy::too_many_arguments)]
319 fn new(
320 client: HyperClient<C, Full<Bytes>>,
321 method: Method,
322 uri: Uri,
323 redirect_policy: RedirectPolicy,
324 retry_policy: Option<RetryPolicy>,
325 decompression: bool,
326 middleware: Vec<Arc<dyn ClientMiddleware>>,
327 cookie_jar: Option<Arc<std::sync::Mutex<oxihttp_core::CookieJar>>>,
328 ) -> Self {
329 Self {
330 client,
331 method,
332 uri,
333 headers: HeaderMap::new(),
334 body: Bytes::new(),
335 timeout: None,
336 redirect_policy,
337 retry_policy,
338 decompression,
339 middleware,
340 cookie_jar,
341 }
342 }
343
344 pub fn header(mut self, key: &str, value: &str) -> Result<Self, OxiHttpError> {
346 let k =
347 HeaderName::from_str(key).map_err(|e| OxiHttpError::InvalidHeader(e.to_string()))?;
348 let v =
349 HeaderValue::from_str(value).map_err(|e| OxiHttpError::InvalidHeader(e.to_string()))?;
350 self.headers.insert(k, v);
351 Ok(self)
352 }
353
354 pub fn headers(mut self, map: HeaderMap) -> Self {
356 self.headers.extend(map);
357 self
358 }
359
360 pub fn bearer_token(mut self, token: &str) -> Result<Self, OxiHttpError> {
362 let v = HeaderValue::from_str(&format!("Bearer {token}"))
363 .map_err(|e| OxiHttpError::InvalidHeader(e.to_string()))?;
364 self.headers.insert(http::header::AUTHORIZATION, v);
365 Ok(self)
366 }
367
368 pub fn basic_auth(
370 mut self,
371 username: &str,
372 password: Option<&str>,
373 ) -> Result<Self, OxiHttpError> {
374 let credentials = match password {
375 Some(pw) => format!("{username}:{pw}"),
376 None => format!("{username}:"),
377 };
378 let encoded = base64_encode(credentials.as_bytes());
379 let v = HeaderValue::from_str(&format!("Basic {encoded}"))
380 .map_err(|e| OxiHttpError::InvalidHeader(e.to_string()))?;
381 self.headers.insert(http::header::AUTHORIZATION, v);
382 Ok(self)
383 }
384
385 pub fn body(mut self, b: impl Into<Bytes>) -> Self {
387 self.body = b.into();
388 self
389 }
390
391 pub fn json<T: serde::Serialize>(mut self, value: &T) -> Result<Self, OxiHttpError> {
393 let json_bytes =
394 serde_json::to_vec(value).map_err(|e| OxiHttpError::Json(e.to_string()))?;
395 self.body = Bytes::from(json_bytes);
396 let ct = HeaderValue::from_static("application/json");
397 self.headers.insert(http::header::CONTENT_TYPE, ct);
398 Ok(self)
399 }
400
401 pub fn form(mut self, form_body: &oxihttp_core::FormBody) -> Self {
403 self.body = form_body.clone().build();
404 if let Ok(ct) = HeaderValue::from_str("application/x-www-form-urlencoded") {
405 self.headers.insert(http::header::CONTENT_TYPE, ct);
406 }
407 self
408 }
409
410 pub fn multipart(mut self, builder: oxihttp_core::MultipartBuilder) -> Self {
436 let ct_str = builder.content_type();
438 self.body = builder.build();
439 if !self.headers.contains_key(http::header::CONTENT_TYPE) {
441 if let Ok(ct) = HeaderValue::from_str(&ct_str) {
442 self.headers.insert(http::header::CONTENT_TYPE, ct);
443 }
444 }
445 self
446 }
447
448 pub fn timeout(mut self, duration: Duration) -> Self {
450 self.timeout = Some(duration);
451 self
452 }
453
454 pub async fn send(self) -> Result<Response, OxiHttpError> {
461 let RequestBuilder {
462 client,
463 method,
464 uri,
465 headers,
466 body,
467 timeout,
468 redirect_policy,
469 retry_policy,
470 decompression,
471 middleware,
472 cookie_jar,
473 } = self;
474
475 {
477 let ctx = middleware::RequestContext {
478 method: &method,
479 uri: &uri,
480 headers: &headers,
481 };
482 for mw in &middleware {
483 mw.before_request(&ctx);
484 }
485 }
486
487 let start = Instant::now();
488
489 let max_attempts = retry_policy
490 .as_ref()
491 .map(|p| p.max_retries + 1)
492 .unwrap_or(1);
493
494 for attempt in 0..max_attempts {
495 let result = {
496 let fut = send_inner(
497 &client,
498 method.clone(),
499 uri.clone(),
500 body.clone(),
501 headers.clone(),
502 &redirect_policy,
503 decompression,
504 cookie_jar.clone(),
505 );
506 if let Some(dur) = timeout {
507 match tokio::time::timeout(dur, fut).await {
508 Ok(r) => r,
509 Err(_) => Err(OxiHttpError::Timeout(format!(
510 "request timed out after {}ms",
511 dur.as_millis()
512 ))),
513 }
514 } else {
515 fut.await
516 }
517 };
518
519 match result {
520 Ok(resp) => {
521 if let Some(ref policy) = retry_policy {
522 if attempt < max_attempts - 1
523 && policy.should_retry_status(resp.status().as_u16())
524 {
525 let delay = policy.backoff_delay(attempt);
526 tokio::time::sleep(delay).await;
527 continue;
528 }
529 }
530 let elapsed = start.elapsed();
532 let resp_ctx = middleware::ResponseContext {
533 status: resp.status(),
534 elapsed,
535 };
536 for mw in &middleware {
537 mw.after_response(&resp_ctx);
538 }
539 return Ok(resp);
540 }
541 Err(e) => {
542 if let Some(ref policy) = retry_policy {
543 let should_retry = match &e {
544 OxiHttpError::Hyper(_) => policy.retry_on_connection_error,
545 OxiHttpError::Timeout(_) => policy.retry_on_timeout,
546 OxiHttpError::Io(_) => policy.retry_on_connection_error,
547 _ => false,
548 };
549 if should_retry && attempt < max_attempts - 1 {
550 let delay = policy.backoff_delay(attempt);
551 tokio::time::sleep(delay).await;
552 continue;
553 }
554 }
555 return Err(e);
556 }
557 }
558 }
559
560 Err(OxiHttpError::Hyper("max retries exceeded".to_string()))
562 }
563}
564
565#[allow(clippy::too_many_arguments)]
570async fn send_inner<C>(
571 client: &HyperClient<C, Full<Bytes>>,
572 mut method: Method,
573 mut uri: Uri,
574 mut body: Bytes,
575 headers: HeaderMap,
576 redirect_policy: &RedirectPolicy,
577 decompression: bool,
578 cookie_jar: Option<Arc<std::sync::Mutex<oxihttp_core::CookieJar>>>,
579) -> Result<Response, OxiHttpError>
580where
581 C: Connect + Clone + Send + Sync + 'static,
582{
583 let max_redirects = redirect_policy.max_redirects();
584 let mut redirect_count: usize = 0;
585
586 loop {
587 let mut req_builder = http::Request::builder()
588 .method(method.clone())
589 .uri(uri.clone());
590 for (k, v) in &headers {
591 req_builder = req_builder.header(k, v);
592 }
593
594 if decompression && !headers.contains_key(http::header::ACCEPT_ENCODING) {
597 req_builder = req_builder.header(
598 http::header::ACCEPT_ENCODING,
599 HeaderValue::from_static("gzip, deflate"),
600 );
601 }
602
603 let mut req = req_builder
604 .body(Full::new(body.clone()))
605 .map_err(|e| OxiHttpError::Http(Arc::new(e)))?;
606
607 if let Some(ref jar) = cookie_jar {
609 if let Ok(guard) = jar.lock() {
610 if let Some(cookie_header) = guard.to_cookie_header_for_url(&uri) {
611 if let Ok(hv) = HeaderValue::from_str(&cookie_header) {
612 req.headers_mut().insert(http::header::COOKIE, hv);
613 }
614 }
615 }
616 }
617
618 let resp = client
619 .request(req)
620 .await
621 .map_err(|e| OxiHttpError::Hyper(e.to_string()))?;
622
623 if let Some(ref jar) = cookie_jar {
625 if let Ok(mut guard) = jar.lock() {
626 guard.add_from_response_headers(resp.headers(), &uri);
627 }
628 }
629
630 let status = resp.status();
632 if redirect::is_redirect_status(status) {
633 if let Some(max) = max_redirects {
634 if max == 0 || redirect_count >= max {
635 if max == 0 {
637 return Ok(Response {
638 inner: resp,
639 decompress: decompression,
640 });
641 }
642 return Err(OxiHttpError::Redirect(format!(
643 "too many redirects (max: {max})"
644 )));
645 }
646 }
647 redirect_count += 1;
648
649 let location = resp
651 .headers()
652 .get(http::header::LOCATION)
653 .and_then(|v| v.to_str().ok())
654 .ok_or_else(|| {
655 OxiHttpError::Redirect("redirect response missing Location header".to_string())
656 })?;
657
658 let new_uri = resolve_redirect_uri(&uri, location)?;
660
661 let new_method = redirect::redirect_method(status, &method);
663
664 if !redirect::should_preserve_body(status) {
666 body = Bytes::new();
667 }
668
669 method = new_method;
670 uri = new_uri;
671 continue;
672 }
673
674 return Ok(Response {
675 inner: resp,
676 decompress: decompression,
677 });
678 }
679}
680
681fn resolve_redirect_uri(base: &Uri, location: &str) -> Result<Uri, OxiHttpError> {
683 if let Ok(uri) = Uri::from_str(location) {
685 if uri.scheme().is_some() {
686 return Ok(uri);
687 }
688 }
689
690 let scheme = base.scheme_str().unwrap_or("http");
692 let authority = base.authority().map(|a| a.as_str()).unwrap_or("localhost");
693 let full = format!("{scheme}://{authority}{location}");
694 Uri::from_str(&full).map_err(|e| OxiHttpError::InvalidUri(Arc::new(e)))
695}
696
697#[cfg(feature = "tls")]
700#[derive(Clone)]
701pub(crate) struct TlsRebuildConfig {
702 pub trusted_certs_der: Vec<Vec<u8>>,
703 pub alpn: Vec<String>,
704 pub accept_invalid_certs: bool,
705 pub use_webpki_roots: bool,
706 pub key_log_path: Option<std::path::PathBuf>,
707 pub early_data: bool,
708 pub connect_timeout: Option<Duration>,
709 pub tcp_nodelay: Option<bool>,
710 pub tcp_keepalive: Option<Duration>,
711 pub http2_settings: Option<Http2Settings>,
712 pub pool_max_idle_per_host: Option<usize>,
713 pub pool_idle_timeout: Option<Duration>,
714 pub custom_cert_verifier: Option<Arc<dyn rustls::client::danger::ServerCertVerifier>>,
718}
719
720#[cfg(feature = "tls")]
721impl std::fmt::Debug for TlsRebuildConfig {
722 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
723 f.debug_struct("TlsRebuildConfig")
724 .field("trusted_certs_der_count", &self.trusted_certs_der.len())
725 .field("alpn", &self.alpn)
726 .field("accept_invalid_certs", &self.accept_invalid_certs)
727 .field("use_webpki_roots", &self.use_webpki_roots)
728 .field("early_data", &self.early_data)
729 .field("connect_timeout", &self.connect_timeout)
730 .field("tcp_nodelay", &self.tcp_nodelay)
731 .field("tcp_keepalive", &self.tcp_keepalive)
732 .field(
733 "custom_cert_verifier",
734 &self
735 .custom_cert_verifier
736 .as_ref()
737 .map(|_| "<dyn ServerCertVerifier>"),
738 )
739 .finish_non_exhaustive()
740 }
741}
742
743#[derive(Clone)]
754pub struct Client<C = HttpConnector> {
755 pub(crate) inner: HyperClient<C, Full<Bytes>>,
756 pub(crate) redirect_policy: RedirectPolicy,
757 pub(crate) retry_policy: Option<RetryPolicy>,
758 pub(crate) default_headers: HeaderMap,
759 pub(crate) connect_timeout: Option<Duration>,
760 pub(crate) read_timeout: Option<Duration>,
761 pub(crate) decompression: bool,
762 pub(crate) middleware: Vec<Arc<dyn ClientMiddleware>>,
764 pub(crate) cookie_jar: Option<Arc<std::sync::Mutex<oxihttp_core::CookieJar>>>,
766 #[cfg(feature = "tls")]
771 pub(crate) tls_rebuild: Option<Arc<TlsRebuildConfig>>,
772}
773
774#[cfg(feature = "tls")]
778pub type HttpsClient = Client<OxiHttpsConnector<HttpConnector>>;
779
780pub type ResolverClient = Client<HttpConnector<BoxResolver>>;
784
785#[cfg(feature = "tls")]
789pub type ResolverHttpsClient =
790 Client<crate::connector::OxiHttpsConnector<HttpConnector<BoxResolver>>>;
791
792impl Client<HttpConnector> {
795 pub fn builder() -> ClientBuilder {
797 ClientBuilder::new()
798 }
799}
800
801#[cfg(feature = "tls")]
810impl Client<OxiHttpsConnector<HttpConnector>> {
811 pub fn with_request_tls_config(
848 &self,
849 override_cfg: RequestTlsConfig,
850 ) -> Result<Self, OxiHttpError> {
851 use crate::connector::OxiHttpsConnector;
852
853 let base = self.tls_rebuild.as_ref().ok_or_else(|| {
854 OxiHttpError::Tls(
855 "client has no TLS rebuild config (was it built with build_https()?)".to_string(),
856 )
857 })?;
858
859 let effective_certs = if override_cfg.trusted_cert_ders.is_empty() {
861 base.trusted_certs_der.as_slice()
862 } else {
863 override_cfg.trusted_cert_ders.as_slice()
864 };
865 let accept_invalid = base.accept_invalid_certs || override_cfg.accept_invalid_certs;
866
867 let new_tls = if let Some(ref verifier) = base.custom_cert_verifier {
870 tls::build_tls_connector_with_verifier(
871 Arc::clone(verifier),
872 &base.alpn,
873 base.early_data,
874 )?
875 } else {
876 tls::build_tls_connector(
877 effective_certs,
878 &base.alpn,
879 accept_invalid,
880 base.use_webpki_roots,
881 base.key_log_path.clone(),
882 base.early_data,
883 )?
884 };
885
886 let mut http = HttpConnector::new();
887 http.enforce_http(false);
888 if let Some(dur) = base.connect_timeout {
889 http.set_connect_timeout(Some(dur));
890 }
891 if let Some(nodelay) = base.tcp_nodelay {
892 http.set_nodelay(nodelay);
893 }
894 if let Some(ka) = base.tcp_keepalive {
895 http.set_keepalive(Some(ka));
896 }
897 let https_connector = OxiHttpsConnector::new(http, new_tls);
898
899 let mut hb = HyperClient::builder(TokioExecutor::new());
900 if let Some(n) = base.pool_max_idle_per_host {
901 hb.pool_max_idle_per_host(n);
902 }
903 if let Some(dur) = base.pool_idle_timeout {
904 hb.pool_idle_timeout(dur);
905 }
906 if let Some(ref h2) = base.http2_settings {
907 apply_http2_settings(&mut hb, h2);
908 }
909
910 let new_rebuild = Arc::new(TlsRebuildConfig {
914 trusted_certs_der: effective_certs.to_vec(),
915 alpn: base.alpn.clone(),
916 accept_invalid_certs: accept_invalid,
917 use_webpki_roots: base.use_webpki_roots,
918 key_log_path: base.key_log_path.clone(),
919 early_data: base.early_data,
920 connect_timeout: base.connect_timeout,
921 tcp_nodelay: base.tcp_nodelay,
922 tcp_keepalive: base.tcp_keepalive,
923 http2_settings: base.http2_settings.clone(),
924 pool_max_idle_per_host: base.pool_max_idle_per_host,
925 pool_idle_timeout: base.pool_idle_timeout,
926 custom_cert_verifier: base.custom_cert_verifier.clone(),
927 });
928
929 Ok(Client {
930 inner: hb.build(https_connector),
931 redirect_policy: self.redirect_policy.clone(),
932 retry_policy: self.retry_policy.clone(),
933 default_headers: self.default_headers.clone(),
934 connect_timeout: self.connect_timeout,
935 read_timeout: self.read_timeout,
936 decompression: self.decompression,
937 middleware: self.middleware.clone(),
938 cookie_jar: self.cookie_jar.clone(),
939 tls_rebuild: Some(new_rebuild),
940 })
941 }
942}
943
944impl<C> Client<C>
945where
946 C: Connect + Clone + Send + Sync + 'static,
947{
948 fn request_builder(
950 &self,
951 method: Method,
952 url: &str,
953 ) -> Result<RequestBuilder<C>, OxiHttpError> {
954 let uri = Uri::from_str(url)?;
955 let mut rb = RequestBuilder::new(
956 self.inner.clone(),
957 method,
958 uri,
959 self.redirect_policy.clone(),
960 self.retry_policy.clone(),
961 self.decompression,
962 self.middleware.clone(),
963 self.cookie_jar.clone(),
964 );
965 for (k, v) in &self.default_headers {
967 rb.headers.insert(k.clone(), v.clone());
968 }
969 Ok(rb)
970 }
971
972 pub fn get(&self, url: &str) -> Result<RequestBuilder<C>, OxiHttpError> {
974 self.request_builder(Method::GET, url)
975 }
976
977 pub fn post(&self, url: &str) -> Result<RequestBuilder<C>, OxiHttpError> {
979 self.request_builder(Method::POST, url)
980 }
981
982 pub fn put(&self, url: &str) -> Result<RequestBuilder<C>, OxiHttpError> {
984 self.request_builder(Method::PUT, url)
985 }
986
987 pub fn delete(&self, url: &str) -> Result<RequestBuilder<C>, OxiHttpError> {
989 self.request_builder(Method::DELETE, url)
990 }
991
992 pub fn patch(&self, url: &str) -> Result<RequestBuilder<C>, OxiHttpError> {
994 self.request_builder(Method::PATCH, url)
995 }
996
997 pub fn head(&self, url: &str) -> Result<RequestBuilder<C>, OxiHttpError> {
999 self.request_builder(Method::HEAD, url)
1000 }
1001
1002 pub async fn execute(&self, req: http::Request<Full<Bytes>>) -> Result<Response, OxiHttpError> {
1004 let resp = self
1005 .inner
1006 .request(req)
1007 .await
1008 .map_err(|e| OxiHttpError::Hyper(e.to_string()))?;
1009 Ok(Response {
1010 inner: resp,
1011 decompress: self.decompression,
1012 })
1013 }
1014
1015 pub async fn get_bytes(&self, url: &str) -> Result<Bytes, OxiHttpError> {
1017 let resp = self.get(url)?.send().await?;
1018 resp.error_for_status()?.body_bytes().await
1019 }
1020
1021 pub async fn get_json<T: serde::de::DeserializeOwned>(
1023 &self,
1024 url: &str,
1025 ) -> Result<T, OxiHttpError> {
1026 let resp = self.get(url)?.send().await?;
1027 resp.error_for_status()?.body_json().await
1028 }
1029
1030 pub async fn post_json<T: serde::Serialize, R: serde::de::DeserializeOwned>(
1032 &self,
1033 url: &str,
1034 body: &T,
1035 ) -> Result<R, OxiHttpError> {
1036 let resp = self.post(url)?.json(body)?.send().await?;
1037 resp.error_for_status()?.body_json().await
1038 }
1039
1040 pub fn retry_policy(&self) -> Option<&RetryPolicy> {
1042 self.retry_policy.as_ref()
1043 }
1044
1045 pub fn connect_timeout(&self) -> Option<Duration> {
1047 self.connect_timeout
1048 }
1049
1050 pub fn read_timeout(&self) -> Option<Duration> {
1052 self.read_timeout
1053 }
1054}
1055
1056impl<C> std::fmt::Debug for Client<C> {
1057 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1058 f.debug_struct("Client")
1059 .field("redirect_policy", &self.redirect_policy)
1060 .field("retry_policy", &self.retry_policy)
1061 .field("default_headers_count", &self.default_headers.len())
1062 .finish()
1063 }
1064}
1065
1066fn base64_encode(data: &[u8]) -> String {
1068 const CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1069 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
1070 for chunk in data.chunks(3) {
1071 let b0 = chunk[0] as u32;
1072 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
1073 let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
1074 let triple = (b0 << 16) | (b1 << 8) | b2;
1075
1076 result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
1077 result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
1078 if chunk.len() > 1 {
1079 result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
1080 } else {
1081 result.push('=');
1082 }
1083 if chunk.len() > 2 {
1084 result.push(CHARS[(triple & 0x3F) as usize] as char);
1085 } else {
1086 result.push('=');
1087 }
1088 }
1089 result
1090}
1091
1092#[cfg(test)]
1097mod tests {
1098 use super::*;
1099 use oxihttp_core::MultipartBuilder;
1100
1101 fn post_builder() -> RequestBuilder {
1105 let client = Client::builder().build().expect("client build");
1106 client
1107 .post("http://127.0.0.1:0/test")
1108 .expect("request builder")
1109 }
1110
1111 #[test]
1114 fn multipart_sets_content_type_automatically() {
1115 let mp = MultipartBuilder::new().add_text("field", "value");
1116 let expected_boundary = mp.boundary().to_owned();
1118
1119 let rb = post_builder().multipart(mp);
1120
1121 let ct = rb
1122 .headers
1123 .get(http::header::CONTENT_TYPE)
1124 .and_then(|v| v.to_str().ok())
1125 .expect("Content-Type header must be set after .multipart()");
1126
1127 assert!(
1128 ct.starts_with("multipart/form-data; boundary="),
1129 "Content-Type must start with multipart/form-data; boundary= but got: {ct}"
1130 );
1131 assert!(
1132 ct.contains(&expected_boundary),
1133 "Content-Type must contain the boundary '{expected_boundary}' but got: {ct}"
1134 );
1135 }
1136
1137 #[test]
1140 fn multipart_does_not_override_explicit_content_type() {
1141 let mp = MultipartBuilder::new().add_text("x", "y");
1142
1143 let rb = post_builder()
1144 .header("content-type", "application/octet-stream")
1145 .expect("header set")
1146 .multipart(mp);
1147
1148 let ct = rb
1149 .headers
1150 .get(http::header::CONTENT_TYPE)
1151 .and_then(|v| v.to_str().ok())
1152 .expect("Content-Type header must be present");
1153
1154 assert_eq!(
1155 ct, "application/octet-stream",
1156 "explicit Content-Type must not be overridden by .multipart()"
1157 );
1158 }
1159}