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(crate) 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
43#[cfg(feature = "socks")]
44pub use proxy::Socks5Connector;
45pub use proxy::{ProxyConnector, ProxyKind};
46
47use bytes::Bytes;
48use futures_core::Stream;
49use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri};
50use http_body_util::{BodyExt, Full};
51use hyper::body::Incoming;
52use hyper_util::client::legacy::connect::{Connect, HttpConnector};
53use hyper_util::client::legacy::Client as HyperClient;
54#[cfg(feature = "tls")]
55use hyper_util::rt::TokioExecutor;
56use resolver::BoxResolver;
57use std::pin::Pin;
58use std::str::FromStr;
59use std::sync::Arc;
60use std::task::{Context, Poll};
61use std::time::{Duration, Instant};
62
63#[cfg(feature = "tls")]
64pub(crate) use client_builder::apply_http2_settings;
65pub use client_builder::{ClientBuilder, Http2Settings};
66pub use middleware::{ClientMiddleware, LoggingMiddleware, TimingMiddleware};
67use oxihttp_core::OxiHttpError;
68pub use redirect::RedirectPolicy;
69pub use retry::RetryPolicy;
70
71pub struct BodyStream {
77 inner: http_body_util::BodyStream<Incoming>,
78}
79
80impl Stream for BodyStream {
81 type Item = Result<Bytes, OxiHttpError>;
82
83 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
84 loop {
85 match Pin::new(&mut self.inner).poll_next(cx) {
86 Poll::Ready(Some(Ok(frame))) => {
87 if let Ok(data) = frame.into_data() {
88 return Poll::Ready(Some(Ok(data)));
89 }
90 }
92 Poll::Ready(Some(Err(e))) => {
93 return Poll::Ready(Some(Err(OxiHttpError::Body(e.to_string()))));
94 }
95 Poll::Ready(None) => return Poll::Ready(None),
96 Poll::Pending => return Poll::Pending,
97 }
98 }
99 }
100}
101
102pub struct Response {
108 inner: http::Response<Incoming>,
109 decompress: bool,
111}
112
113impl Response {
114 pub fn status(&self) -> StatusCode {
116 self.inner.status()
117 }
118
119 pub fn headers(&self) -> &HeaderMap {
121 self.inner.headers()
122 }
123
124 pub fn version(&self) -> http::Version {
126 self.inner.version()
127 }
128
129 pub fn content_length(&self) -> Option<u64> {
131 self.inner
132 .headers()
133 .get(http::header::CONTENT_LENGTH)
134 .and_then(|v| v.to_str().ok())
135 .and_then(|s| s.parse().ok())
136 }
137
138 pub async fn body_bytes(self) -> Result<Bytes, OxiHttpError> {
140 let decompress = self.decompress;
141 let ce = self
142 .inner
143 .headers()
144 .get(http::header::CONTENT_ENCODING)
145 .and_then(|v| v.to_str().ok())
146 .map(|s| s.to_ascii_lowercase());
147
148 let raw = self
149 .inner
150 .into_body()
151 .collect()
152 .await
153 .map(|c| c.to_bytes())
154 .map_err(|e| OxiHttpError::Body(e.to_string()))?;
155
156 if decompress {
157 match ce.as_deref() {
158 Some("gzip") => {
159 #[cfg(feature = "decompression")]
160 {
161 let decompressed = oxiarc_deflate::gzip_decompress(&raw).map_err(|e| {
162 OxiHttpError::Body(format!("gzip decompression error: {e}"))
163 })?;
164 return Ok(Bytes::from(decompressed));
165 }
166 #[cfg(not(feature = "decompression"))]
167 {
168 }
170 }
171 Some("deflate") => {
172 #[cfg(feature = "decompression")]
173 {
174 let decompressed = oxiarc_deflate::zlib_decompress(&raw)
175 .or_else(|_| {
176 oxiarc_deflate::inflate(&raw).map_err(|e| {
178 OxiHttpError::Body(format!("deflate decompression error: {e}"))
179 })
180 })
181 .map_err(|e| {
182 OxiHttpError::Body(format!("deflate decompression error: {e}"))
183 })?;
184 return Ok(Bytes::from(decompressed));
185 }
186 #[cfg(not(feature = "decompression"))]
187 {
188 }
190 }
191 _ => {}
192 }
193 }
194
195 Ok(raw)
196 }
197
198 pub async fn body_text(self) -> Result<String, OxiHttpError> {
200 let bytes = self.body_bytes().await?;
201 String::from_utf8(bytes.to_vec())
202 .map_err(|e| OxiHttpError::Body(format!("invalid UTF-8: {e}")))
203 }
204
205 pub async fn body_json<T: serde::de::DeserializeOwned>(self) -> Result<T, OxiHttpError> {
207 let bytes = self.body_bytes().await?;
208 serde_json::from_slice(&bytes).map_err(|e| OxiHttpError::Json(e.to_string()))
209 }
210
211 pub fn error_for_status(self) -> Result<Self, OxiHttpError> {
215 let status = self.inner.status();
216 if status.is_client_error() || status.is_server_error() {
217 Err(OxiHttpError::Body(format!(
218 "HTTP error: {} {}",
219 status.as_u16(),
220 status.canonical_reason().unwrap_or("Unknown")
221 )))
222 } else {
223 Ok(self)
224 }
225 }
226
227 pub fn content_type(&self) -> Option<&str> {
229 self.inner
230 .headers()
231 .get(http::header::CONTENT_TYPE)
232 .and_then(|v| v.to_str().ok())
233 }
234
235 pub fn cookies(&self) -> Vec<oxihttp_core::Cookie> {
240 self.inner
241 .headers()
242 .get_all(http::header::SET_COOKIE)
243 .iter()
244 .filter_map(|v| v.to_str().ok())
245 .filter_map(oxihttp_core::Cookie::parse_set_cookie)
246 .collect()
247 }
248
249 pub fn body_stream(self) -> BodyStream {
251 BodyStream {
252 inner: http_body_util::BodyStream::new(self.inner.into_body()),
253 }
254 }
255}
256
257impl std::fmt::Debug for Response {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 f.debug_struct("Response")
260 .field("status", &self.inner.status())
261 .field("version", &self.inner.version())
262 .field("headers", self.inner.headers())
263 .finish()
264 }
265}
266
267pub struct RequestBuilder<C = HttpConnector> {
275 client: HyperClient<C, Full<Bytes>>,
276 method: Method,
277 uri: Uri,
278 headers: HeaderMap,
279 body: Bytes,
280 timeout: Option<Duration>,
281 redirect_policy: RedirectPolicy,
282 retry_policy: Option<RetryPolicy>,
283 decompression: bool,
284 middleware: Vec<Arc<dyn ClientMiddleware>>,
285 cookie_jar: Option<Arc<std::sync::Mutex<oxihttp_core::CookieJar>>>,
286}
287
288impl<C> RequestBuilder<C>
289where
290 C: Connect + Clone + Send + Sync + 'static,
291{
292 #[allow(clippy::too_many_arguments)]
293 fn new(
294 client: HyperClient<C, Full<Bytes>>,
295 method: Method,
296 uri: Uri,
297 redirect_policy: RedirectPolicy,
298 retry_policy: Option<RetryPolicy>,
299 decompression: bool,
300 middleware: Vec<Arc<dyn ClientMiddleware>>,
301 cookie_jar: Option<Arc<std::sync::Mutex<oxihttp_core::CookieJar>>>,
302 ) -> Self {
303 Self {
304 client,
305 method,
306 uri,
307 headers: HeaderMap::new(),
308 body: Bytes::new(),
309 timeout: None,
310 redirect_policy,
311 retry_policy,
312 decompression,
313 middleware,
314 cookie_jar,
315 }
316 }
317
318 pub fn header(mut self, key: &str, value: &str) -> Result<Self, OxiHttpError> {
320 let k =
321 HeaderName::from_str(key).map_err(|e| OxiHttpError::InvalidHeader(e.to_string()))?;
322 let v =
323 HeaderValue::from_str(value).map_err(|e| OxiHttpError::InvalidHeader(e.to_string()))?;
324 self.headers.insert(k, v);
325 Ok(self)
326 }
327
328 pub fn headers(mut self, map: HeaderMap) -> Self {
330 self.headers.extend(map);
331 self
332 }
333
334 pub fn bearer_token(mut self, token: &str) -> Result<Self, OxiHttpError> {
336 let v = HeaderValue::from_str(&format!("Bearer {token}"))
337 .map_err(|e| OxiHttpError::InvalidHeader(e.to_string()))?;
338 self.headers.insert(http::header::AUTHORIZATION, v);
339 Ok(self)
340 }
341
342 pub fn basic_auth(
344 mut self,
345 username: &str,
346 password: Option<&str>,
347 ) -> Result<Self, OxiHttpError> {
348 let credentials = match password {
349 Some(pw) => format!("{username}:{pw}"),
350 None => format!("{username}:"),
351 };
352 let encoded = base64_encode(credentials.as_bytes());
353 let v = HeaderValue::from_str(&format!("Basic {encoded}"))
354 .map_err(|e| OxiHttpError::InvalidHeader(e.to_string()))?;
355 self.headers.insert(http::header::AUTHORIZATION, v);
356 Ok(self)
357 }
358
359 pub fn body(mut self, b: impl Into<Bytes>) -> Self {
361 self.body = b.into();
362 self
363 }
364
365 pub fn json<T: serde::Serialize>(mut self, value: &T) -> Result<Self, OxiHttpError> {
367 let json_bytes =
368 serde_json::to_vec(value).map_err(|e| OxiHttpError::Json(e.to_string()))?;
369 self.body = Bytes::from(json_bytes);
370 let ct = HeaderValue::from_static("application/json");
371 self.headers.insert(http::header::CONTENT_TYPE, ct);
372 Ok(self)
373 }
374
375 pub fn form(mut self, form_body: &oxihttp_core::FormBody) -> Self {
377 self.body = form_body.clone().build();
378 if let Ok(ct) = HeaderValue::from_str("application/x-www-form-urlencoded") {
379 self.headers.insert(http::header::CONTENT_TYPE, ct);
380 }
381 self
382 }
383
384 pub fn multipart(mut self, builder: oxihttp_core::MultipartBuilder) -> Self {
410 let ct_str = builder.content_type();
412 self.body = builder.build();
413 if !self.headers.contains_key(http::header::CONTENT_TYPE) {
415 if let Ok(ct) = HeaderValue::from_str(&ct_str) {
416 self.headers.insert(http::header::CONTENT_TYPE, ct);
417 }
418 }
419 self
420 }
421
422 pub fn timeout(mut self, duration: Duration) -> Self {
424 self.timeout = Some(duration);
425 self
426 }
427
428 pub async fn send(self) -> Result<Response, OxiHttpError> {
435 let RequestBuilder {
436 client,
437 method,
438 uri,
439 headers,
440 body,
441 timeout,
442 redirect_policy,
443 retry_policy,
444 decompression,
445 middleware,
446 cookie_jar,
447 } = self;
448
449 {
451 let ctx = middleware::RequestContext {
452 method: &method,
453 uri: &uri,
454 headers: &headers,
455 };
456 for mw in &middleware {
457 mw.before_request(&ctx);
458 }
459 }
460
461 let start = Instant::now();
462
463 let max_attempts = retry_policy
464 .as_ref()
465 .map(|p| p.max_retries + 1)
466 .unwrap_or(1);
467
468 for attempt in 0..max_attempts {
469 let result = {
470 let fut = send_inner(
471 &client,
472 method.clone(),
473 uri.clone(),
474 body.clone(),
475 headers.clone(),
476 &redirect_policy,
477 decompression,
478 cookie_jar.clone(),
479 );
480 if let Some(dur) = timeout {
481 match tokio::time::timeout(dur, fut).await {
482 Ok(r) => r,
483 Err(_) => Err(OxiHttpError::Timeout(format!(
484 "request timed out after {}ms",
485 dur.as_millis()
486 ))),
487 }
488 } else {
489 fut.await
490 }
491 };
492
493 match result {
494 Ok(resp) => {
495 if let Some(ref policy) = retry_policy {
496 if attempt < max_attempts - 1
497 && policy.should_retry_status(resp.status().as_u16())
498 {
499 let delay = policy.backoff_delay(attempt);
500 tokio::time::sleep(delay).await;
501 continue;
502 }
503 }
504 let elapsed = start.elapsed();
506 let resp_ctx = middleware::ResponseContext {
507 status: resp.status(),
508 elapsed,
509 };
510 for mw in &middleware {
511 mw.after_response(&resp_ctx);
512 }
513 return Ok(resp);
514 }
515 Err(e) => {
516 if let Some(ref policy) = retry_policy {
517 let should_retry = match &e {
518 OxiHttpError::Hyper(_) => policy.retry_on_connection_error,
519 OxiHttpError::Timeout(_) => policy.retry_on_timeout,
520 OxiHttpError::Io(_) => policy.retry_on_connection_error,
521 _ => false,
522 };
523 if should_retry && attempt < max_attempts - 1 {
524 let delay = policy.backoff_delay(attempt);
525 tokio::time::sleep(delay).await;
526 continue;
527 }
528 }
529 return Err(e);
530 }
531 }
532 }
533
534 Err(OxiHttpError::Hyper("max retries exceeded".to_string()))
536 }
537}
538
539#[allow(clippy::too_many_arguments)]
544async fn send_inner<C>(
545 client: &HyperClient<C, Full<Bytes>>,
546 mut method: Method,
547 mut uri: Uri,
548 mut body: Bytes,
549 headers: HeaderMap,
550 redirect_policy: &RedirectPolicy,
551 decompression: bool,
552 cookie_jar: Option<Arc<std::sync::Mutex<oxihttp_core::CookieJar>>>,
553) -> Result<Response, OxiHttpError>
554where
555 C: Connect + Clone + Send + Sync + 'static,
556{
557 let max_redirects = redirect_policy.max_redirects();
558 let mut redirect_count: usize = 0;
559
560 loop {
561 let mut req_builder = http::Request::builder()
562 .method(method.clone())
563 .uri(uri.clone());
564 for (k, v) in &headers {
565 req_builder = req_builder.header(k, v);
566 }
567
568 if decompression && !headers.contains_key(http::header::ACCEPT_ENCODING) {
571 req_builder = req_builder.header(
572 http::header::ACCEPT_ENCODING,
573 HeaderValue::from_static("gzip, deflate"),
574 );
575 }
576
577 let mut req = req_builder
578 .body(Full::new(body.clone()))
579 .map_err(|e| OxiHttpError::Http(Arc::new(e)))?;
580
581 if let Some(ref jar) = cookie_jar {
583 if let Ok(guard) = jar.lock() {
584 if let Some(cookie_header) = guard.to_cookie_header_for_url(&uri) {
585 if let Ok(hv) = HeaderValue::from_str(&cookie_header) {
586 req.headers_mut().insert(http::header::COOKIE, hv);
587 }
588 }
589 }
590 }
591
592 let resp = client
593 .request(req)
594 .await
595 .map_err(|e| OxiHttpError::Hyper(e.to_string()))?;
596
597 if let Some(ref jar) = cookie_jar {
599 if let Ok(mut guard) = jar.lock() {
600 guard.add_from_response_headers(resp.headers(), &uri);
601 }
602 }
603
604 let status = resp.status();
606 if redirect::is_redirect_status(status) {
607 if let Some(max) = max_redirects {
608 if max == 0 || redirect_count >= max {
609 if max == 0 {
611 return Ok(Response {
612 inner: resp,
613 decompress: decompression,
614 });
615 }
616 return Err(OxiHttpError::Redirect(format!(
617 "too many redirects (max: {max})"
618 )));
619 }
620 }
621 redirect_count += 1;
622
623 let location = resp
625 .headers()
626 .get(http::header::LOCATION)
627 .and_then(|v| v.to_str().ok())
628 .ok_or_else(|| {
629 OxiHttpError::Redirect("redirect response missing Location header".to_string())
630 })?;
631
632 let new_uri = resolve_redirect_uri(&uri, location)?;
634
635 let new_method = redirect::redirect_method(status, &method);
637
638 if !redirect::should_preserve_body(status) {
640 body = Bytes::new();
641 }
642
643 method = new_method;
644 uri = new_uri;
645 continue;
646 }
647
648 return Ok(Response {
649 inner: resp,
650 decompress: decompression,
651 });
652 }
653}
654
655fn resolve_redirect_uri(base: &Uri, location: &str) -> Result<Uri, OxiHttpError> {
657 if let Ok(uri) = Uri::from_str(location) {
659 if uri.scheme().is_some() {
660 return Ok(uri);
661 }
662 }
663
664 let scheme = base.scheme_str().unwrap_or("http");
666 let authority = base.authority().map(|a| a.as_str()).unwrap_or("localhost");
667 let full = format!("{scheme}://{authority}{location}");
668 Uri::from_str(&full).map_err(|e| OxiHttpError::InvalidUri(Arc::new(e)))
669}
670
671#[cfg(feature = "tls")]
674#[derive(Debug, Clone)]
675pub(crate) struct TlsRebuildConfig {
676 pub trusted_certs_der: Vec<Vec<u8>>,
677 pub alpn: Vec<String>,
678 pub accept_invalid_certs: bool,
679 pub use_webpki_roots: bool,
680 pub key_log_path: Option<std::path::PathBuf>,
681 pub early_data: bool,
682 pub connect_timeout: Option<Duration>,
683 pub tcp_nodelay: Option<bool>,
684 pub tcp_keepalive: Option<Duration>,
685 pub http2_settings: Option<Http2Settings>,
686 pub pool_max_idle_per_host: Option<usize>,
687 pub pool_idle_timeout: Option<Duration>,
688}
689
690#[derive(Clone)]
701pub struct Client<C = HttpConnector> {
702 pub(crate) inner: HyperClient<C, Full<Bytes>>,
703 pub(crate) redirect_policy: RedirectPolicy,
704 pub(crate) retry_policy: Option<RetryPolicy>,
705 pub(crate) default_headers: HeaderMap,
706 pub(crate) connect_timeout: Option<Duration>,
707 pub(crate) read_timeout: Option<Duration>,
708 pub(crate) decompression: bool,
709 pub(crate) middleware: Vec<Arc<dyn ClientMiddleware>>,
711 pub(crate) cookie_jar: Option<Arc<std::sync::Mutex<oxihttp_core::CookieJar>>>,
713 #[cfg(feature = "tls")]
718 pub(crate) tls_rebuild: Option<Arc<TlsRebuildConfig>>,
719}
720
721#[cfg(feature = "tls")]
725pub type HttpsClient = Client<OxiHttpsConnector<HttpConnector>>;
726
727pub type ResolverClient = Client<HttpConnector<BoxResolver>>;
731
732#[cfg(feature = "tls")]
736pub type ResolverHttpsClient =
737 Client<crate::connector::OxiHttpsConnector<HttpConnector<BoxResolver>>>;
738
739impl Client<HttpConnector> {
742 pub fn builder() -> ClientBuilder {
744 ClientBuilder::new()
745 }
746}
747
748#[cfg(feature = "tls")]
757impl Client<OxiHttpsConnector<HttpConnector>> {
758 pub fn with_request_tls_config(
795 &self,
796 override_cfg: RequestTlsConfig,
797 ) -> Result<Self, OxiHttpError> {
798 use crate::connector::OxiHttpsConnector;
799
800 let base = self.tls_rebuild.as_ref().ok_or_else(|| {
801 OxiHttpError::Tls(
802 "client has no TLS rebuild config (was it built with build_https()?)".to_string(),
803 )
804 })?;
805
806 let effective_certs = if override_cfg.trusted_cert_ders.is_empty() {
808 base.trusted_certs_der.as_slice()
809 } else {
810 override_cfg.trusted_cert_ders.as_slice()
811 };
812 let accept_invalid = base.accept_invalid_certs || override_cfg.accept_invalid_certs;
813
814 let new_tls = tls::build_tls_connector(
815 effective_certs,
816 &base.alpn,
817 accept_invalid,
818 base.use_webpki_roots,
819 base.key_log_path.clone(),
820 base.early_data,
821 )?;
822
823 let mut http = HttpConnector::new();
824 http.enforce_http(false);
825 if let Some(dur) = base.connect_timeout {
826 http.set_connect_timeout(Some(dur));
827 }
828 if let Some(nodelay) = base.tcp_nodelay {
829 http.set_nodelay(nodelay);
830 }
831 if let Some(ka) = base.tcp_keepalive {
832 http.set_keepalive(Some(ka));
833 }
834 let https_connector = OxiHttpsConnector::new(http, new_tls);
835
836 let mut hb = HyperClient::builder(TokioExecutor::new());
837 if let Some(n) = base.pool_max_idle_per_host {
838 hb.pool_max_idle_per_host(n);
839 }
840 if let Some(dur) = base.pool_idle_timeout {
841 hb.pool_idle_timeout(dur);
842 }
843 if let Some(ref h2) = base.http2_settings {
844 apply_http2_settings(&mut hb, h2);
845 }
846
847 let new_rebuild = Arc::new(TlsRebuildConfig {
851 trusted_certs_der: effective_certs.to_vec(),
852 alpn: base.alpn.clone(),
853 accept_invalid_certs: accept_invalid,
854 use_webpki_roots: base.use_webpki_roots,
855 key_log_path: base.key_log_path.clone(),
856 early_data: base.early_data,
857 connect_timeout: base.connect_timeout,
858 tcp_nodelay: base.tcp_nodelay,
859 tcp_keepalive: base.tcp_keepalive,
860 http2_settings: base.http2_settings.clone(),
861 pool_max_idle_per_host: base.pool_max_idle_per_host,
862 pool_idle_timeout: base.pool_idle_timeout,
863 });
864
865 Ok(Client {
866 inner: hb.build(https_connector),
867 redirect_policy: self.redirect_policy.clone(),
868 retry_policy: self.retry_policy.clone(),
869 default_headers: self.default_headers.clone(),
870 connect_timeout: self.connect_timeout,
871 read_timeout: self.read_timeout,
872 decompression: self.decompression,
873 middleware: self.middleware.clone(),
874 cookie_jar: self.cookie_jar.clone(),
875 tls_rebuild: Some(new_rebuild),
876 })
877 }
878}
879
880impl<C> Client<C>
881where
882 C: Connect + Clone + Send + Sync + 'static,
883{
884 fn request_builder(
886 &self,
887 method: Method,
888 url: &str,
889 ) -> Result<RequestBuilder<C>, OxiHttpError> {
890 let uri = Uri::from_str(url)?;
891 let mut rb = RequestBuilder::new(
892 self.inner.clone(),
893 method,
894 uri,
895 self.redirect_policy.clone(),
896 self.retry_policy.clone(),
897 self.decompression,
898 self.middleware.clone(),
899 self.cookie_jar.clone(),
900 );
901 for (k, v) in &self.default_headers {
903 rb.headers.insert(k.clone(), v.clone());
904 }
905 Ok(rb)
906 }
907
908 pub fn get(&self, url: &str) -> Result<RequestBuilder<C>, OxiHttpError> {
910 self.request_builder(Method::GET, url)
911 }
912
913 pub fn post(&self, url: &str) -> Result<RequestBuilder<C>, OxiHttpError> {
915 self.request_builder(Method::POST, url)
916 }
917
918 pub fn put(&self, url: &str) -> Result<RequestBuilder<C>, OxiHttpError> {
920 self.request_builder(Method::PUT, url)
921 }
922
923 pub fn delete(&self, url: &str) -> Result<RequestBuilder<C>, OxiHttpError> {
925 self.request_builder(Method::DELETE, url)
926 }
927
928 pub fn patch(&self, url: &str) -> Result<RequestBuilder<C>, OxiHttpError> {
930 self.request_builder(Method::PATCH, url)
931 }
932
933 pub fn head(&self, url: &str) -> Result<RequestBuilder<C>, OxiHttpError> {
935 self.request_builder(Method::HEAD, url)
936 }
937
938 pub async fn execute(&self, req: http::Request<Full<Bytes>>) -> Result<Response, OxiHttpError> {
940 let resp = self
941 .inner
942 .request(req)
943 .await
944 .map_err(|e| OxiHttpError::Hyper(e.to_string()))?;
945 Ok(Response {
946 inner: resp,
947 decompress: self.decompression,
948 })
949 }
950
951 pub async fn get_bytes(&self, url: &str) -> Result<Bytes, OxiHttpError> {
953 let resp = self.get(url)?.send().await?;
954 resp.error_for_status()?.body_bytes().await
955 }
956
957 pub async fn get_json<T: serde::de::DeserializeOwned>(
959 &self,
960 url: &str,
961 ) -> Result<T, OxiHttpError> {
962 let resp = self.get(url)?.send().await?;
963 resp.error_for_status()?.body_json().await
964 }
965
966 pub async fn post_json<T: serde::Serialize, R: serde::de::DeserializeOwned>(
968 &self,
969 url: &str,
970 body: &T,
971 ) -> Result<R, OxiHttpError> {
972 let resp = self.post(url)?.json(body)?.send().await?;
973 resp.error_for_status()?.body_json().await
974 }
975
976 pub fn retry_policy(&self) -> Option<&RetryPolicy> {
978 self.retry_policy.as_ref()
979 }
980
981 pub fn connect_timeout(&self) -> Option<Duration> {
983 self.connect_timeout
984 }
985
986 pub fn read_timeout(&self) -> Option<Duration> {
988 self.read_timeout
989 }
990}
991
992impl<C> std::fmt::Debug for Client<C> {
993 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
994 f.debug_struct("Client")
995 .field("redirect_policy", &self.redirect_policy)
996 .field("retry_policy", &self.retry_policy)
997 .field("default_headers_count", &self.default_headers.len())
998 .finish()
999 }
1000}
1001
1002fn base64_encode(data: &[u8]) -> String {
1004 const CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1005 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
1006 for chunk in data.chunks(3) {
1007 let b0 = chunk[0] as u32;
1008 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
1009 let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
1010 let triple = (b0 << 16) | (b1 << 8) | b2;
1011
1012 result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
1013 result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
1014 if chunk.len() > 1 {
1015 result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
1016 } else {
1017 result.push('=');
1018 }
1019 if chunk.len() > 2 {
1020 result.push(CHARS[(triple & 0x3F) as usize] as char);
1021 } else {
1022 result.push('=');
1023 }
1024 }
1025 result
1026}
1027
1028#[cfg(test)]
1033mod tests {
1034 use super::*;
1035 use oxihttp_core::MultipartBuilder;
1036
1037 fn post_builder() -> RequestBuilder {
1041 let client = Client::builder().build().expect("client build");
1042 client
1043 .post("http://127.0.0.1:0/test")
1044 .expect("request builder")
1045 }
1046
1047 #[test]
1050 fn multipart_sets_content_type_automatically() {
1051 let mp = MultipartBuilder::new().add_text("field", "value");
1052 let expected_boundary = mp.boundary().to_owned();
1054
1055 let rb = post_builder().multipart(mp);
1056
1057 let ct = rb
1058 .headers
1059 .get(http::header::CONTENT_TYPE)
1060 .and_then(|v| v.to_str().ok())
1061 .expect("Content-Type header must be set after .multipart()");
1062
1063 assert!(
1064 ct.starts_with("multipart/form-data; boundary="),
1065 "Content-Type must start with multipart/form-data; boundary= but got: {ct}"
1066 );
1067 assert!(
1068 ct.contains(&expected_boundary),
1069 "Content-Type must contain the boundary '{expected_boundary}' but got: {ct}"
1070 );
1071 }
1072
1073 #[test]
1076 fn multipart_does_not_override_explicit_content_type() {
1077 let mp = MultipartBuilder::new().add_text("x", "y");
1078
1079 let rb = post_builder()
1080 .header("content-type", "application/octet-stream")
1081 .expect("header set")
1082 .multipart(mp);
1083
1084 let ct = rb
1085 .headers
1086 .get(http::header::CONTENT_TYPE)
1087 .and_then(|v| v.to_str().ok())
1088 .expect("Content-Type header must be present");
1089
1090 assert_eq!(
1091 ct, "application/octet-stream",
1092 "explicit Content-Type must not be overridden by .multipart()"
1093 );
1094 }
1095}