1use bytes::Bytes;
42use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri};
43use http_body_util::{BodyExt, Full};
44use hyper::body::Incoming;
45use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer};
46use serde::{de::DeserializeOwned, Serialize};
47use std::collections::HashMap;
48use std::net::{IpAddr, SocketAddr};
49use std::sync::{Arc, Mutex};
50use std::time::{Duration, Instant};
51use tokio::io::{AsyncRead, AsyncWrite};
52use tokio::net::TcpStream;
53
54#[derive(Debug)]
56#[non_exhaustive]
57pub enum ClientError {
58 InvalidUrl(String),
60 Connect(String),
62 Tls(String),
64 Blocked(IpAddr),
66 Timeout,
68 Protocol(String),
70 TooLarge(usize),
72 TooManyRedirects,
74 Decode(String),
76}
77
78impl std::fmt::Display for ClientError {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 match self {
81 ClientError::InvalidUrl(u) => write!(f, "invalid URL: {u}"),
82 ClientError::Connect(e) => write!(f, "connection failed: {e}"),
83 ClientError::Tls(e) => write!(f, "TLS error: {e}"),
84 ClientError::Blocked(ip) => write!(f, "target address {ip} is not public"),
85 ClientError::Timeout => f.write_str("request timed out"),
86 ClientError::Protocol(e) => write!(f, "invalid response: {e}"),
87 ClientError::TooLarge(max) => write!(f, "response larger than {max} bytes"),
88 ClientError::TooManyRedirects => f.write_str("too many redirects"),
89 ClientError::Decode(e) => write!(f, "cannot decode response: {e}"),
90 }
91 }
92}
93
94impl std::error::Error for ClientError {}
95
96impl From<ClientError> for crate::error::Error {
97 fn from(e: ClientError) -> Self {
98 let status = if matches!(e, ClientError::Timeout) {
99 504
100 } else {
101 502
102 };
103 crate::error::Error::custom(status, format!("Upstream request failed: {e}"))
104 }
105}
106
107trait Io: AsyncRead + AsyncWrite + Unpin + Send {}
108impl<T: AsyncRead + AsyncWrite + Unpin + Send> Io for T {}
109
110enum Sender {
113 H1(hyper::client::conn::http1::SendRequest<Full<Bytes>>),
114 H2(hyper::client::conn::http2::SendRequest<Full<Bytes>>),
115}
116
117impl Sender {
118 fn is_h2(&self) -> bool {
119 matches!(self, Sender::H2(_))
120 }
121
122 fn is_closed(&self) -> bool {
123 match self {
124 Sender::H1(s) => s.is_closed(),
125 Sender::H2(s) => s.is_closed(),
126 }
127 }
128
129 fn duplicate(&self) -> Option<Sender> {
130 match self {
131 Sender::H1(_) => None,
132 Sender::H2(s) => Some(Sender::H2(s.clone())),
133 }
134 }
135
136 async fn send(
137 &mut self,
138 req: http::Request<Full<Bytes>>,
139 ) -> Result<http::Response<Incoming>, hyper::Error> {
140 match self {
141 Sender::H1(s) => s.send_request(req).await,
142 Sender::H2(s) => s.send_request(req).await,
143 }
144 }
145}
146
147#[derive(Hash, PartialEq, Eq, Clone)]
148struct PoolKey {
149 https: bool,
150 host: String,
151 port: u16,
152}
153
154struct Idle {
155 sender: Sender,
156 since: Instant,
157}
158
159struct Shared {
160 pool: Mutex<HashMap<PoolKey, Vec<Idle>>>,
161 #[cfg(feature = "client-tls")]
163 tls: tokio_rustls::TlsConnector,
164 #[cfg(feature = "client-tls")]
166 tls_h1: tokio_rustls::TlsConnector,
167}
168
169#[derive(Clone)]
171pub struct HttpClient {
172 shared: Arc<Shared>,
173 timeout: Duration,
174 connect_timeout: Duration,
175 max_response: usize,
176 max_redirects: usize,
177 retries: u32,
178 backoff: Duration,
179 public_only: bool,
180 user_agent: String,
181 headers: HeaderMap,
182 idle_timeout: Duration,
183 max_idle_per_host: usize,
184 http2: bool,
185 h2_prior_knowledge: bool,
186}
187
188impl Default for HttpClient {
189 fn default() -> Self {
190 Self::new()
191 }
192}
193
194impl HttpClient {
195 pub fn new() -> Self {
196 Self {
197 shared: Arc::new(Shared {
198 pool: Mutex::new(HashMap::new()),
199 #[cfg(feature = "client-tls")]
200 tls: tls_connectors(&[]).expect("default TLS configuration").0,
201 #[cfg(feature = "client-tls")]
202 tls_h1: tls_connectors(&[]).expect("default TLS configuration").1,
203 }),
204 timeout: Duration::from_secs(30),
205 connect_timeout: Duration::from_secs(10),
206 max_response: 10 * 1024 * 1024,
207 max_redirects: 5,
208 retries: 0,
209 backoff: Duration::from_millis(200),
210 public_only: false,
211 user_agent: concat!("ullun-api/", env!("CARGO_PKG_VERSION")).to_string(),
212 headers: HeaderMap::new(),
213 idle_timeout: Duration::from_secs(30),
214 max_idle_per_host: 8,
215 http2: true,
216 h2_prior_knowledge: false,
217 }
218 }
219
220 pub fn timeout(mut self, timeout: Duration) -> Self {
222 self.timeout = timeout;
223 self
224 }
225
226 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
228 self.connect_timeout = timeout;
229 self
230 }
231
232 pub fn max_response_bytes(mut self, max: usize) -> Self {
234 self.max_response = max;
235 self
236 }
237
238 pub fn max_redirects(mut self, max: usize) -> Self {
240 self.max_redirects = max;
241 self
242 }
243
244 pub fn retries(mut self, retries: u32) -> Self {
246 self.retries = retries;
247 self
248 }
249
250 pub fn retry_backoff(mut self, base: Duration) -> Self {
252 self.backoff = base;
253 self
254 }
255
256 pub fn public_only(mut self) -> Self {
260 self.public_only = true;
261 self
262 }
263
264 pub fn user_agent(mut self, agent: &str) -> Self {
265 self.user_agent = agent.to_string();
266 self
267 }
268
269 pub fn default_header(mut self, name: &str, value: &str) -> Self {
271 if let (Ok(n), Ok(v)) = (
272 HeaderName::from_bytes(name.as_bytes()),
273 HeaderValue::from_str(value),
274 ) {
275 self.headers.insert(n, v);
276 }
277 self
278 }
279
280 pub fn pool_idle_timeout(mut self, timeout: Duration) -> Self {
282 self.idle_timeout = timeout;
283 self
284 }
285
286 pub fn http1_only(mut self) -> Self {
290 self.http2 = false;
291 self
292 }
293
294 pub fn http2_prior_knowledge(mut self) -> Self {
298 self.h2_prior_knowledge = true;
299 self
300 }
301
302 #[cfg(feature = "client-tls")]
305 pub fn add_root_certificates_pem(mut self, pem: &[u8]) -> Result<Self, ClientError> {
306 let (tls, tls_h1) = tls_connectors(&[pem])?;
307 let shared = Shared {
308 pool: Mutex::new(HashMap::new()),
309 tls,
310 tls_h1,
311 };
312 self.shared = Arc::new(shared);
313 Ok(self)
314 }
315
316 pub fn request(&self, method: Method, url: &str) -> RequestBuilder {
317 RequestBuilder {
318 client: self.clone(),
319 method,
320 url: url.to_string(),
321 headers: HeaderMap::new(),
322 body: Bytes::new(),
323 timeout: None,
324 error: None,
325 }
326 }
327
328 pub fn get(&self, url: &str) -> RequestBuilder {
329 self.request(Method::GET, url)
330 }
331 pub fn post(&self, url: &str) -> RequestBuilder {
332 self.request(Method::POST, url)
333 }
334 pub fn put(&self, url: &str) -> RequestBuilder {
335 self.request(Method::PUT, url)
336 }
337 pub fn patch(&self, url: &str) -> RequestBuilder {
338 self.request(Method::PATCH, url)
339 }
340 pub fn delete(&self, url: &str) -> RequestBuilder {
341 self.request(Method::DELETE, url)
342 }
343 pub fn head(&self, url: &str) -> RequestBuilder {
344 self.request(Method::HEAD, url)
345 }
346}
347
348pub struct RequestBuilder {
350 client: HttpClient,
351 method: Method,
352 url: String,
353 headers: HeaderMap,
354 body: Bytes,
355 timeout: Option<Duration>,
356 error: Option<ClientError>,
357}
358
359impl RequestBuilder {
360 pub fn header(mut self, name: &str, value: &str) -> Self {
361 match (
362 HeaderName::from_bytes(name.as_bytes()),
363 HeaderValue::from_str(value),
364 ) {
365 (Ok(n), Ok(v)) => {
366 self.headers.insert(n, v);
367 }
368 _ => self.error = Some(ClientError::InvalidUrl(format!("invalid header {name}"))),
369 }
370 self
371 }
372
373 pub fn bearer(self, token: &str) -> Self {
374 self.header("authorization", &format!("Bearer {token}"))
375 }
376
377 pub fn json<T: Serialize>(mut self, value: &T) -> Self {
378 match serde_json::to_vec(value) {
379 Ok(body) => {
380 self.body = Bytes::from(body);
381 self.header("content-type", "application/json")
382 }
383 Err(e) => {
384 self.error = Some(ClientError::Decode(e.to_string()));
385 self
386 }
387 }
388 }
389
390 pub fn form<T: Serialize>(mut self, value: &T) -> Self {
391 match serde_urlencoded::to_string(value) {
392 Ok(body) => {
393 self.body = Bytes::from(body);
394 self.header("content-type", "application/x-www-form-urlencoded")
395 }
396 Err(e) => {
397 self.error = Some(ClientError::Decode(e.to_string()));
398 self
399 }
400 }
401 }
402
403 pub fn body(mut self, body: impl Into<Bytes>) -> Self {
404 self.body = body.into();
405 self
406 }
407
408 pub fn query<T: Serialize>(mut self, params: &T) -> Self {
410 match serde_urlencoded::to_string(params) {
411 Ok(q) if !q.is_empty() => {
412 self.url
413 .push(if self.url.contains('?') { '&' } else { '?' });
414 self.url.push_str(&q);
415 }
416 Ok(_) => {}
417 Err(e) => self.error = Some(ClientError::Decode(e.to_string())),
418 }
419 self
420 }
421
422 pub fn trace(self, incoming: &crate::request::Request) -> Self {
427 let Some(ctx) = incoming.trace() else {
428 return self;
429 };
430 let this = self.header("traceparent", &ctx.traceparent());
431 match &ctx.tracestate {
432 Some(state) => this.header("tracestate", state),
433 None => this,
434 }
435 }
436
437 pub fn timeout(mut self, timeout: Duration) -> Self {
439 self.timeout = Some(timeout);
440 self
441 }
442
443 pub async fn send(self) -> Result<ClientResponse, ClientError> {
444 if let Some(e) = self.error {
445 return Err(e);
446 }
447 let c = &self.client;
448 let timeout = self.timeout.unwrap_or(c.timeout);
449 let repeatable = is_repeatable(&self.method, &self.headers);
450 let mut attempt = 0;
451 loop {
452 let outcome = match tokio::time::timeout(timeout, c.run(&self)).await {
453 Ok(r) => r,
454 Err(_) => Err(ClientError::Timeout),
455 };
456 let retry_after = match &outcome {
457 Ok(res) if retryable_status(res.status) => Some(res.retry_after()),
458 Ok(_) => None,
459 Err(e) if retryable_error(e) => Some(None),
460 Err(_) => None,
461 };
462 match retry_after {
463 Some(hint) if repeatable && attempt < c.retries => {
464 let wait = hint.unwrap_or_else(|| c.delay(attempt));
465 tokio::time::sleep(wait.min(Duration::from_secs(10))).await;
467 attempt += 1;
468 }
469 _ => return outcome,
470 }
471 }
472 }
473}
474
475fn is_repeatable(method: &Method, headers: &HeaderMap) -> bool {
476 matches!(
477 *method,
478 Method::GET | Method::HEAD | Method::PUT | Method::DELETE | Method::OPTIONS
479 ) || headers.contains_key("idempotency-key")
480}
481
482fn retryable_status(status: StatusCode) -> bool {
483 matches!(status.as_u16(), 429 | 502 | 503 | 504)
484}
485
486fn retryable_error(e: &ClientError) -> bool {
487 matches!(
488 e,
489 ClientError::Connect(_) | ClientError::Timeout | ClientError::Protocol(_)
490 )
491}
492
493impl HttpClient {
494 fn delay(&self, attempt: u32) -> Duration {
496 let base = self.backoff.saturating_mul(1 << attempt.min(8));
497 let jitter = crate::security::random_bytes(1)
498 .ok()
499 .and_then(|b| b.first().copied())
500 .unwrap_or(0) as f64
501 / 255.0
502 * 0.5;
503 base.mul_f64(1.0 + jitter)
504 }
505
506 async fn run(&self, req: &RequestBuilder) -> Result<ClientResponse, ClientError> {
508 let mut url = req.url.clone();
509 let mut method = req.method.clone();
510 let mut body = req.body.clone();
511 let mut headers = req.headers.clone();
512 let origin = origin_of(&req.url)?;
513 for _ in 0..=self.max_redirects {
514 let uri: Uri = url
515 .parse()
516 .map_err(|_| ClientError::InvalidUrl(url.clone()))?;
517 let res = self.exchange(&uri, &method, &headers, &body).await?;
518 let status = res.status;
519 let location = res
520 .headers
521 .get(http::header::LOCATION)
522 .and_then(|v| v.to_str().ok());
523 if self.max_redirects == 0 || !status.is_redirection() || location.is_none() {
524 return Ok(res);
525 }
526 let next = resolve_location(&uri, location.unwrap_or_default())
527 .ok_or_else(|| ClientError::Protocol("bad redirect location".into()))?;
528 if matches!(status.as_u16(), 301..=303) && method != Method::HEAD {
530 method = Method::GET;
531 body = Bytes::new();
532 headers.remove(http::header::CONTENT_TYPE);
533 headers.remove(http::header::CONTENT_LENGTH);
534 }
535 if origin_of(&next)? != origin {
536 headers.remove(http::header::AUTHORIZATION);
538 headers.remove(http::header::COOKIE);
539 }
540 url = next;
541 }
542 Err(ClientError::TooManyRedirects)
543 }
544
545 async fn exchange(
546 &self,
547 uri: &Uri,
548 method: &Method,
549 extra: &HeaderMap,
550 body: &Bytes,
551 ) -> Result<ClientResponse, ClientError> {
552 let (https, host, port) = target(uri)?;
553 let key = PoolKey {
554 https,
555 host: host.clone(),
556 port,
557 };
558 let path = uri
559 .path_and_query()
560 .map(|p| p.as_str())
561 .unwrap_or("/")
562 .to_string();
563
564 let build = |h2: bool| -> Result<http::Request<Full<Bytes>>, ClientError> {
565 let host_value = if (https && port == 443) || (!https && port == 80) {
566 host.clone()
567 } else {
568 format!("{host}:{port}")
569 };
570 let target = if h2 {
572 format!(
573 "{}://{host_value}{path}",
574 if https { "https" } else { "http" }
575 )
576 } else {
577 path.clone()
578 };
579 let mut b = http::Request::builder().method(method.clone()).uri(target);
580 let h = b.headers_mut().expect("builder has headers");
581 if !h2 {
582 h.insert(
583 http::header::HOST,
584 HeaderValue::from_str(&host_value)
585 .map_err(|_| ClientError::InvalidUrl(uri.to_string()))?,
586 );
587 }
588 if let Ok(agent) = HeaderValue::from_str(&self.user_agent) {
589 h.insert(http::header::USER_AGENT, agent);
590 }
591 for (n, v) in self.headers.iter().chain(extra.iter()) {
592 if h2 && is_connection_specific(n) {
594 continue;
595 }
596 h.insert(n.clone(), v.clone());
597 }
598 if !body.is_empty() || matches!(*method, Method::POST | Method::PUT | Method::PATCH) {
599 h.insert(http::header::CONTENT_LENGTH, body.len().into());
600 }
601 b.body(Full::new(body.clone()))
602 .map_err(|e| ClientError::Protocol(e.to_string()))
603 };
604
605 let mut sender = self.checkout(&key);
608 let mut from_pool = sender.is_some();
609 let mut response = None;
610 if let Some(s) = sender.as_mut() {
611 match s.send(build(s.is_h2())?).await {
612 Ok(r) => response = Some(r),
613 Err(_) => {
614 sender = None;
615 from_pool = false;
616 }
617 }
618 }
619 let response = match response {
620 Some(r) => r,
621 None => {
622 let mut fresh = self.connect(&key).await?;
623 let r = fresh
624 .send(build(fresh.is_h2())?)
625 .await
626 .map_err(|e| ClientError::Protocol(e.to_string()))?;
627 sender = Some(fresh);
628 r
629 }
630 };
631
632 let (parts, incoming) = response.into_parts();
633 let bytes = read_limited(incoming, self.max_response).await?;
634 if let Some(s) = sender {
635 let keep = s.is_h2() && !from_pool
637 || (!s.is_h2()
638 && parts.version == http::Version::HTTP_11
639 && !parts
640 .headers
641 .get(http::header::CONNECTION)
642 .and_then(|v| v.to_str().ok())
643 .is_some_and(|v| v.eq_ignore_ascii_case("close")));
644 if keep {
645 self.checkin(key, s);
646 }
647 }
648 Ok(ClientResponse {
649 status: parts.status,
650 version: parts.version,
651 headers: parts.headers,
652 body: bytes,
653 })
654 }
655
656 fn checkout(&self, key: &PoolKey) -> Option<Sender> {
657 let mut pool = self.shared.pool.lock().unwrap_or_else(|e| e.into_inner());
658 let list = pool.get_mut(key)?;
659 list.retain(|i| !i.sender.is_closed());
660 if let Some(idle) = list.iter_mut().find(|i| i.sender.is_h2()) {
662 idle.since = Instant::now();
663 return idle.sender.duplicate();
664 }
665 while let Some(idle) = list.pop() {
666 if !idle.sender.is_closed() && idle.since.elapsed() < self.idle_timeout {
667 return Some(idle.sender);
668 }
669 }
670 None
671 }
672
673 fn checkin(&self, key: PoolKey, sender: Sender) {
674 if self.idle_timeout.is_zero() || sender.is_closed() {
675 return;
676 }
677 let mut pool = self.shared.pool.lock().unwrap_or_else(|e| e.into_inner());
678 let list = pool.entry(key).or_default();
679 list.retain(|i| !i.sender.is_closed() && i.since.elapsed() < self.idle_timeout);
680 if sender.is_h2() && list.iter().any(|i| i.sender.is_h2()) {
682 return;
683 }
684 if list.len() < self.max_idle_per_host {
685 list.push(Idle {
686 sender,
687 since: Instant::now(),
688 });
689 }
690 }
691
692 async fn connect(&self, key: &PoolKey) -> Result<Sender, ClientError> {
693 #[cfg(not(feature = "client-tls"))]
694 if key.https {
695 return Err(ClientError::Tls(
696 "https needs the `client-tls` feature of ullun-api".into(),
697 ));
698 }
699 let connect = async {
700 let addrs: Vec<SocketAddr> = tokio::net::lookup_host((key.host.as_str(), key.port))
701 .await
702 .map_err(|e| ClientError::Connect(e.to_string()))?
703 .collect();
704 if addrs.is_empty() {
705 return Err(ClientError::Connect("name resolution failed".into()));
706 }
707 if self.public_only {
710 if let Some(bad) = addrs
711 .iter()
712 .find(|a| !crate::security::is_public_ip(a.ip()))
713 {
714 return Err(ClientError::Blocked(bad.ip()));
715 }
716 }
717 let mut last = String::new();
719 let mut connected = None;
720 for addr in addrs {
721 match TcpStream::connect(addr).await {
722 Ok(tcp) => {
723 connected = Some(tcp);
724 break;
725 }
726 Err(e) => last = e.to_string(),
727 }
728 }
729 let tcp = connected.ok_or(ClientError::Connect(last))?;
730 let _ = tcp.set_nodelay(true);
731 let (io, h2): (Box<dyn Io>, bool) = if key.https {
732 self.tls(tcp, &key.host).await?
733 } else {
734 (Box::new(tcp), self.h2_prior_knowledge)
735 };
736 if h2 {
737 let (sender, conn) = hyper::client::conn::http2::Builder::new(TokioExecutor::new())
738 .timer(TokioTimer::new())
739 .keep_alive_interval(Duration::from_secs(30))
741 .keep_alive_timeout(Duration::from_secs(20))
742 .handshake(TokioIo::new(io))
743 .await
744 .map_err(|e| ClientError::Protocol(e.to_string()))?;
745 tokio::spawn(async move {
746 let _ = conn.await;
747 });
748 return Ok::<_, ClientError>(Sender::H2(sender));
749 }
750 let (sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(io))
751 .await
752 .map_err(|e| ClientError::Protocol(e.to_string()))?;
753 tokio::spawn(async move {
754 let _ = conn.await;
755 });
756 Ok::<_, ClientError>(Sender::H1(sender))
757 };
758 tokio::time::timeout(self.connect_timeout, connect)
759 .await
760 .map_err(|_| ClientError::Timeout)?
761 }
762
763 #[cfg(feature = "client-tls")]
764 async fn tls(&self, tcp: TcpStream, host: &str) -> Result<(Box<dyn Io>, bool), ClientError> {
765 let name = tokio_rustls::rustls::pki_types::ServerName::try_from(host.to_string())
766 .map_err(|_| ClientError::Tls(format!("invalid server name {host}")))?;
767 let connector = if self.http2 {
768 &self.shared.tls
769 } else {
770 &self.shared.tls_h1
771 };
772 let stream = connector
773 .connect(name, tcp)
774 .await
775 .map_err(|e| ClientError::Tls(e.to_string()))?;
776 let h2 = stream.get_ref().1.alpn_protocol() == Some(b"h2");
777 Ok((Box::new(stream), h2))
778 }
779
780 #[cfg(not(feature = "client-tls"))]
781 async fn tls(&self, _tcp: TcpStream, _host: &str) -> Result<(Box<dyn Io>, bool), ClientError> {
782 Err(ClientError::Tls(
783 "https needs the `client-tls` feature of ullun-api".into(),
784 ))
785 }
786}
787
788#[cfg(feature = "client-tls")]
789fn tls_connectors(
790 extra_pem: &[&[u8]],
791) -> Result<(tokio_rustls::TlsConnector, tokio_rustls::TlsConnector), ClientError> {
792 use tokio_rustls::rustls::pki_types::pem::PemObject;
793 use tokio_rustls::rustls::pki_types::CertificateDer;
794 use tokio_rustls::rustls::{ClientConfig, RootCertStore};
795
796 let mut roots = RootCertStore::empty();
797 roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
798 for pem in extra_pem {
799 let mut added = 0;
800 for cert in CertificateDer::pem_slice_iter(pem) {
801 let cert = cert.map_err(|e| ClientError::Tls(e.to_string()))?;
802 roots
803 .add(cert)
804 .map_err(|e| ClientError::Tls(e.to_string()))?;
805 added += 1;
806 }
807 if added == 0 {
808 return Err(ClientError::Tls("no certificate in the PEM data".into()));
809 }
810 }
811 let mut config = ClientConfig::builder_with_provider(Arc::new(
812 tokio_rustls::rustls::crypto::ring::default_provider(),
813 ))
814 .with_safe_default_protocol_versions()
815 .map_err(|e| ClientError::Tls(e.to_string()))?
816 .with_root_certificates(roots)
817 .with_no_client_auth();
818 let mut http1_only = config.clone();
819 http1_only.alpn_protocols = vec![b"http/1.1".to_vec()];
820 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
821 Ok((
822 tokio_rustls::TlsConnector::from(Arc::new(config)),
823 tokio_rustls::TlsConnector::from(Arc::new(http1_only)),
824 ))
825}
826
827fn is_connection_specific(name: &HeaderName) -> bool {
829 matches!(
830 name.as_str(),
831 "connection" | "keep-alive" | "proxy-connection" | "transfer-encoding" | "upgrade" | "host"
832 )
833}
834
835async fn read_limited(mut body: Incoming, max: usize) -> Result<Bytes, ClientError> {
836 use bytes::BufMut;
837 use hyper::body::Body;
838 if body.size_hint().lower() > max as u64 {
839 return Err(ClientError::TooLarge(max));
840 }
841 let mut out = bytes::BytesMut::new();
842 while let Some(frame) = body.frame().await {
843 let frame = frame.map_err(|e| ClientError::Protocol(e.to_string()))?;
844 if let Some(data) = frame.data_ref() {
845 if out.len() + data.len() > max {
846 return Err(ClientError::TooLarge(max));
847 }
848 out.put_slice(data);
849 }
850 }
851 Ok(out.freeze())
852}
853
854fn target(uri: &Uri) -> Result<(bool, String, u16), ClientError> {
856 let https = match uri.scheme_str() {
857 Some("http") => false,
858 Some("https") => true,
859 _ => return Err(ClientError::InvalidUrl(uri.to_string())),
860 };
861 let host = uri
862 .host()
863 .filter(|h| !h.is_empty())
864 .ok_or_else(|| ClientError::InvalidUrl(uri.to_string()))?;
865 let host = host
867 .trim_start_matches('[')
868 .trim_end_matches(']')
869 .to_string();
870 let port = uri.port_u16().unwrap_or(if https { 443 } else { 80 });
871 Ok((https, host, port))
872}
873
874fn origin_of(url: &str) -> Result<(bool, String, u16), ClientError> {
875 let uri: Uri = url
876 .parse()
877 .map_err(|_| ClientError::InvalidUrl(url.to_string()))?;
878 target(&uri)
879}
880
881fn resolve_location(base: &Uri, location: &str) -> Option<String> {
883 if location.starts_with("http://") || location.starts_with("https://") {
884 return Some(location.to_string());
885 }
886 let scheme = base.scheme_str()?;
887 let authority = base.authority()?;
888 if location.starts_with('/') {
889 return Some(format!("{scheme}://{authority}{location}"));
890 }
891 let dir = base.path().rsplit_once('/').map(|(d, _)| d).unwrap_or("");
893 Some(format!("{scheme}://{authority}{dir}/{location}"))
894}
895
896#[derive(Debug, Clone)]
898pub struct ClientResponse {
899 status: StatusCode,
900 version: http::Version,
901 headers: HeaderMap,
902 body: Bytes,
903}
904
905impl ClientResponse {
906 pub fn version(&self) -> http::Version {
908 self.version
909 }
910
911 pub fn status(&self) -> u16 {
912 self.status.as_u16()
913 }
914
915 pub fn is_success(&self) -> bool {
916 self.status.is_success()
917 }
918
919 pub fn headers(&self) -> &HeaderMap {
920 &self.headers
921 }
922
923 pub fn header(&self, name: &str) -> &str {
925 self.headers
926 .get(name)
927 .and_then(|v| v.to_str().ok())
928 .unwrap_or("")
929 }
930
931 pub fn bytes(&self) -> &Bytes {
932 &self.body
933 }
934
935 pub fn text(&self) -> Result<String, ClientError> {
936 String::from_utf8(self.body.to_vec()).map_err(|e| ClientError::Decode(e.to_string()))
937 }
938
939 pub fn json<T: DeserializeOwned>(&self) -> Result<T, ClientError> {
940 serde_json::from_slice(&self.body).map_err(|e| ClientError::Decode(e.to_string()))
941 }
942
943 fn retry_after(&self) -> Option<Duration> {
945 self.header("retry-after")
946 .trim()
947 .parse::<u64>()
948 .ok()
949 .map(Duration::from_secs)
950 }
951}