1use std::{
7 convert::Infallible,
8 error::Error as StdError,
9 future::Future,
10 sync::{Arc, RwLock},
11 time::Duration,
12};
13
14use cookie::Cookie;
15use cookie_store::CookieStore;
16use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, Uri, header};
17use http_body_util::{BodyExt, Full, combinators::BoxBody};
18use hyper::{Response as HyperResponse, body::Incoming};
19use hyper_rustls::HttpsConnector;
20use hyper_util::{
21 client::legacy::{self, connect::HttpConnector},
22 rt::TokioExecutor,
23};
24use serde::{Serialize, de::DeserializeOwned};
25use url::Url;
26
27pub use http::{StatusCode, Version};
28pub use hyper::body::Bytes;
29
30pub type BoxError = Box<dyn StdError + Send + Sync>;
31
32pub type StandardConnector = HttpsConnector<HttpConnector>;
34
35pub type StandardHyperClient = legacy::Client<StandardConnector, Body>;
37
38#[derive(Clone)]
40pub struct StandardService(StandardHyperClient);
41
42impl StandardService {
43 #[must_use]
45 pub const fn hyper_client(&self) -> &StandardHyperClient {
46 &self.0
47 }
48}
49
50impl HttpService for StandardService {
51 type Error = legacy::Error;
52
53 async fn send(&self, request: Request<Body>) -> Result<HyperResponse<Incoming>, Self::Error> {
54 self.0.request(request).await
55 }
56}
57
58pub type StandardClient = Client<StandardService>;
60
61pub struct StandardClientBuilder {
63 cookie_store: bool,
64 default_headers: HeaderMap,
65 timeout: Option<Duration>,
66 pool_idle_timeout: Option<Option<Duration>>,
67 pool_max_idle_per_host: Option<usize>,
68}
69
70impl StandardClientBuilder {
71 #[must_use]
73 pub fn cookie_store(mut self, enabled: bool) -> Self {
74 self.cookie_store = enabled;
75 self
76 }
77
78 #[must_use]
80 pub fn default_headers(mut self, headers: HeaderMap) -> Self {
81 self.default_headers = headers;
82 self
83 }
84
85 #[must_use]
87 pub fn timeout(mut self, timeout: Duration) -> Self {
88 self.timeout = Some(timeout);
89 self
90 }
91
92 #[must_use]
96 pub fn pool_idle_timeout<D>(mut self, timeout: D) -> Self
97 where
98 D: Into<Option<Duration>>,
99 {
100 self.pool_idle_timeout = Some(timeout.into());
101 self
102 }
103
104 #[must_use]
106 pub fn pool_max_idle_per_host(mut self, max: usize) -> Self {
107 self.pool_max_idle_per_host = Some(max);
108 self
109 }
110
111 #[must_use]
113 pub fn build(self) -> StandardClient {
114 let mut hyper_builder = legacy::Client::builder(TokioExecutor::new());
115 if let Some(timeout) = self.pool_idle_timeout {
116 hyper_builder.pool_idle_timeout(timeout);
117 }
118 if let Some(max) = self.pool_max_idle_per_host {
119 hyper_builder.pool_max_idle_per_host(max);
120 }
121 ClientBuilder {
122 service: StandardService(hyper_builder.build(standard_connector())),
123 cookie_store: self.cookie_store,
124 default_headers: self.default_headers,
125 timeout: self.timeout,
126 }
127 .build()
128 }
129}
130
131#[must_use]
133pub fn standard_connector() -> StandardConnector {
134 let _ = rustls::crypto::ring::default_provider().install_default();
138 hyper_rustls::HttpsConnectorBuilder::new()
139 .with_webpki_roots()
140 .https_or_http()
141 .enable_http1()
142 .enable_http2()
143 .build()
144}
145
146#[must_use]
148pub fn standard_hyper_client() -> StandardHyperClient {
149 legacy::Client::builder(TokioExecutor::new()).build(standard_connector())
150}
151
152#[must_use]
154pub fn standard_service() -> StandardService {
155 StandardService(standard_hyper_client())
156}
157
158#[must_use]
160pub fn standard_client() -> StandardClient {
161 standard_client_builder().build()
162}
163
164#[must_use]
169pub fn standard_client_builder() -> StandardClientBuilder {
170 StandardClientBuilder {
171 cookie_store: true,
172 default_headers: HeaderMap::new(),
173 timeout: None,
174 pool_idle_timeout: None,
175 pool_max_idle_per_host: None,
176 }
177}
178
179pub struct Body(BoxBody<Bytes, BoxError>);
181
182impl Body {
183 #[must_use]
184 pub fn empty() -> Self {
185 Self::from(Bytes::new())
186 }
187}
188
189impl Default for Body {
190 fn default() -> Self {
191 Self::empty()
192 }
193}
194
195impl From<Bytes> for Body {
196 fn from(value: Bytes) -> Self {
197 Self(
198 Full::new(value)
199 .map_err(|error: Infallible| match error {})
200 .boxed(),
201 )
202 }
203}
204
205impl From<Vec<u8>> for Body {
206 fn from(value: Vec<u8>) -> Self {
207 Self::from(Bytes::from(value))
208 }
209}
210
211impl From<String> for Body {
212 fn from(value: String) -> Self {
213 Self::from(Bytes::from(value))
214 }
215}
216
217impl From<&'static str> for Body {
218 fn from(value: &'static str) -> Self {
219 Self::from(Bytes::from_static(value.as_bytes()))
220 }
221}
222
223impl hyper::body::Body for Body {
224 type Data = Bytes;
225 type Error = BoxError;
226
227 fn poll_frame(
228 mut self: std::pin::Pin<&mut Self>,
229 cx: &mut std::task::Context<'_>,
230 ) -> std::task::Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
231 std::pin::Pin::new(&mut self.0).poll_frame(cx)
232 }
233
234 fn is_end_stream(&self) -> bool {
235 self.0.is_end_stream()
236 }
237
238 fn size_hint(&self) -> hyper::body::SizeHint {
239 self.0.size_hint()
240 }
241}
242
243pub trait HttpService: Clone + Send + Sync + 'static {
245 type Error: StdError + Send + Sync + 'static;
246
247 fn send(
248 &self,
249 request: Request<Body>,
250 ) -> impl Future<Output = Result<HyperResponse<Incoming>, Self::Error>> + Send;
251}
252
253impl<S> HttpService for S
254where
255 S: hyper::service::Service<Request<Body>, Response = HyperResponse<Incoming>>
256 + Clone
257 + Send
258 + Sync
259 + 'static,
260 S::Error: StdError + Send + Sync + 'static,
261 S::Future: Send,
262{
263 type Error = S::Error;
264
265 fn send(
266 &self,
267 request: Request<Body>,
268 ) -> impl Future<Output = Result<HyperResponse<Incoming>, Self::Error>> + Send {
269 self.call(request)
270 }
271}
272
273#[derive(Debug, thiserror::Error)]
274#[non_exhaustive]
275pub enum Error {
276 #[error("invalid URL: {0}")]
277 Url(#[from] url::ParseError),
278 #[error("invalid request: {0}")]
279 Request(#[from] http::Error),
280 #[error("invalid header: {0}")]
281 Header(String),
282 #[error("HTTP transport failed")]
283 Transport(#[source] BoxError),
284 #[error("request timed out")]
285 Timeout,
286 #[error("cookie store lock was poisoned")]
287 CookieStore,
288 #[error("response body failed")]
289 Body(#[source] BoxError),
290 #[error("JSON serialization failed: {0}")]
291 Json(#[from] serde_json::Error),
292 #[error("response was not valid UTF-8: {0}")]
293 Utf8(#[from] std::string::FromUtf8Error),
294}
295
296#[derive(Clone)]
298pub struct Client<S> {
299 service: S,
300 cookies: Option<Arc<RwLock<CookieStore>>>,
301 default_headers: HeaderMap,
302 timeout: Option<Duration>,
303}
304
305impl<S: HttpService> Client<S> {
306 #[must_use]
307 pub fn new(service: S) -> Self {
308 Self::builder(service).build()
309 }
310
311 #[must_use]
312 pub fn builder(service: S) -> ClientBuilder<S> {
313 ClientBuilder {
314 service,
315 cookie_store: true,
316 default_headers: HeaderMap::new(),
317 timeout: None,
318 }
319 }
320
321 pub fn request<U>(&self, method: Method, url: U) -> Result<RequestBuilder<S>, Error>
322 where
323 U: AsRef<str>,
324 {
325 let url = Url::parse(url.as_ref())?;
326 Ok(RequestBuilder {
327 client: self.clone(),
328 method,
329 url,
330 headers: HeaderMap::new(),
331 body: Body::empty(),
332 })
333 }
334
335 pub fn get<U: AsRef<str>>(&self, url: U) -> Result<RequestBuilder<S>, Error> {
336 self.request(Method::GET, url)
337 }
338
339 pub fn head<U: AsRef<str>>(&self, url: U) -> Result<RequestBuilder<S>, Error> {
340 self.request(Method::HEAD, url)
341 }
342
343 pub fn post<U: AsRef<str>>(&self, url: U) -> Result<RequestBuilder<S>, Error> {
344 self.request(Method::POST, url)
345 }
346
347 pub fn put<U: AsRef<str>>(&self, url: U) -> Result<RequestBuilder<S>, Error> {
348 self.request(Method::PUT, url)
349 }
350
351 pub fn patch<U: AsRef<str>>(&self, url: U) -> Result<RequestBuilder<S>, Error> {
352 self.request(Method::PATCH, url)
353 }
354
355 pub fn delete<U: AsRef<str>>(&self, url: U) -> Result<RequestBuilder<S>, Error> {
356 self.request(Method::DELETE, url)
357 }
358
359 pub async fn execute(&self, request: Request<Body>) -> Result<Response, Error> {
361 let url = Url::parse(&request.uri().to_string())?;
362 self.execute_url(url, request).await
363 }
364
365 async fn execute_url(&self, url: Url, mut request: Request<Body>) -> Result<Response, Error> {
366 for (name, value) in &self.default_headers {
367 if !request.headers().contains_key(name) {
368 request.headers_mut().insert(name, value.clone());
369 }
370 }
371 if !request.headers().contains_key(header::COOKIE) {
372 if let Some(store) = &self.cookies {
373 let store = store.read().map_err(|_| Error::CookieStore)?;
374 if let Some(value) = cookie_header(&store, &url) {
375 request.headers_mut().insert(
376 header::COOKIE,
377 HeaderValue::from_str(&value)
378 .map_err(|error| Error::Header(error.to_string()))?,
379 );
380 }
381 }
382 }
383 let sent = self.service.send(request);
384 let response = if let Some(timeout) = self.timeout {
385 tokio::time::timeout(timeout, sent)
386 .await
387 .map_err(|_| Error::Timeout)?
388 .map_err(|error| Error::Transport(Box::new(error)))?
389 } else {
390 sent.await
391 .map_err(|error| Error::Transport(Box::new(error)))?
392 };
393 if let Some(store) = &self.cookies {
394 let mut store = store.write().map_err(|_| Error::CookieStore)?;
395 store_response_cookies(&mut store, response.headers(), &url);
396 }
397 Ok(Response(response))
398 }
399}
400
401fn cookie_header(store: &CookieStore, url: &Url) -> Option<String> {
402 let value = store
403 .get_request_values(url)
404 .map(|(name, value)| format!("{name}={value}"))
405 .collect::<Vec<_>>()
406 .join("; ");
407 (!value.is_empty()).then_some(value)
408}
409
410fn store_response_cookies(store: &mut CookieStore, headers: &HeaderMap, url: &Url) {
411 let parsed = headers
412 .get_all(header::SET_COOKIE)
413 .iter()
414 .filter_map(|value| value.to_str().ok())
415 .filter_map(|value| Cookie::parse(value.to_owned()).ok())
416 .map(Cookie::into_owned);
417 store.store_response_cookies(parsed, url);
418}
419
420pub struct ClientBuilder<S> {
421 service: S,
422 cookie_store: bool,
423 default_headers: HeaderMap,
424 timeout: Option<Duration>,
425}
426
427impl<S> ClientBuilder<S> {
428 #[must_use]
429 pub fn cookie_store(mut self, enabled: bool) -> Self {
430 self.cookie_store = enabled;
431 self
432 }
433
434 #[must_use]
435 pub fn default_headers(mut self, headers: HeaderMap) -> Self {
436 self.default_headers = headers;
437 self
438 }
439
440 #[must_use]
441 pub fn timeout(mut self, timeout: Duration) -> Self {
442 self.timeout = Some(timeout);
443 self
444 }
445
446 #[must_use]
447 pub fn build(self) -> Client<S> {
448 Client {
449 service: self.service,
450 cookies: self
451 .cookie_store
452 .then(|| Arc::new(RwLock::new(CookieStore::default()))),
453 default_headers: self.default_headers,
454 timeout: self.timeout,
455 }
456 }
457}
458
459pub struct RequestBuilder<S> {
460 client: Client<S>,
461 method: Method,
462 url: Url,
463 headers: HeaderMap,
464 body: Body,
465}
466
467impl<S: HttpService> RequestBuilder<S> {
468 #[must_use]
469 pub fn body(mut self, body: impl Into<Body>) -> Self {
470 self.body = body.into();
471 self
472 }
473
474 pub fn header<K, V>(mut self, key: K, value: V) -> Result<Self, Error>
475 where
476 HeaderName: TryFrom<K>,
477 <HeaderName as TryFrom<K>>::Error: std::fmt::Display,
478 HeaderValue: TryFrom<V>,
479 <HeaderValue as TryFrom<V>>::Error: std::fmt::Display,
480 {
481 let key = HeaderName::try_from(key).map_err(|e| Error::Header(e.to_string()))?;
482 let value = HeaderValue::try_from(value).map_err(|e| Error::Header(e.to_string()))?;
483 self.headers.insert(key, value);
484 Ok(self)
485 }
486
487 #[must_use]
488 pub fn headers(mut self, headers: HeaderMap) -> Self {
489 self.headers.extend(headers);
490 self
491 }
492
493 pub fn json(mut self, value: &impl Serialize) -> Result<Self, Error> {
494 self.body = Body::from(serde_json::to_vec(value)?);
495 self.headers.insert(
496 header::CONTENT_TYPE,
497 HeaderValue::from_static("application/json"),
498 );
499 Ok(self)
500 }
501
502 pub fn build(self) -> Result<Request<Body>, Error> {
503 let uri: Uri = self
504 .url
505 .as_str()
506 .parse()
507 .map_err(|error: http::uri::InvalidUri| Error::Header(error.to_string()))?;
508 let mut request = Request::builder()
509 .method(self.method)
510 .uri(uri)
511 .body(self.body)?;
512 *request.headers_mut() = self.headers;
513 Ok(request)
514 }
515
516 pub async fn send(self) -> Result<Response, Error> {
517 let client = self.client.clone();
518 let url = self.url.clone();
519 client.execute_url(url, self.build()?).await
520 }
521}
522
523pub struct Response(HyperResponse<Incoming>);
524
525impl Response {
526 #[must_use]
527 pub fn status(&self) -> StatusCode {
528 self.0.status()
529 }
530
531 #[must_use]
532 pub fn headers(&self) -> &HeaderMap {
533 self.0.headers()
534 }
535
536 pub async fn bytes(self) -> Result<Bytes, Error> {
537 self.0
538 .into_body()
539 .collect()
540 .await
541 .map(|body| body.to_bytes())
542 .map_err(|error| Error::Body(Box::new(error)))
543 }
544
545 pub async fn text(self) -> Result<String, Error> {
546 Ok(String::from_utf8(self.bytes().await?.to_vec())?)
547 }
548
549 pub async fn json<T: DeserializeOwned>(self) -> Result<T, Error> {
550 Ok(serde_json::from_slice(&self.bytes().await?)?)
551 }
552
553 #[must_use]
554 pub fn into_inner(self) -> HyperResponse<Incoming> {
555 self.0
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use tokio::{
562 io::{AsyncReadExt, AsyncWriteExt},
563 net::TcpListener,
564 };
565
566 use super::*;
567
568 #[test]
569 fn stores_and_scopes_response_cookies() {
570 let url = Url::parse("https://example.com/account/login").unwrap();
571 let mut headers = HeaderMap::new();
572 headers.append(
573 header::SET_COOKIE,
574 HeaderValue::from_static("session=secret; Path=/account; Secure; HttpOnly"),
575 );
576 let mut store = CookieStore::default();
577 store_response_cookies(&mut store, &headers, &url);
578
579 assert_eq!(
580 cookie_header(&store, &url).as_deref(),
581 Some("session=secret")
582 );
583 assert_eq!(
584 cookie_header(&store, &Url::parse("https://example.com/public").unwrap()),
585 None
586 );
587 assert_eq!(
588 cookie_header(&store, &Url::parse("http://example.com/account").unwrap()),
589 None
590 );
591 }
592
593 #[tokio::test]
594 async fn standard_client_uses_tokio_http_connector() {
595 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
596 let address = listener.local_addr().unwrap();
597 let server = tokio::spawn(async move {
598 let (mut stream, _) = listener.accept().await.unwrap();
599 stream
600 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
601 .await
602 .unwrap();
603 });
604
605 let response = standard_client()
606 .get(format!("http://{address}/"))
607 .unwrap()
608 .send()
609 .await
610 .unwrap();
611
612 assert_eq!(response.status(), StatusCode::OK);
613 assert_eq!(response.text().await.unwrap(), "ok");
614 server.await.unwrap();
615 }
616
617 #[tokio::test]
618 async fn standard_client_reuses_pooled_connections() {
619 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
620 let address = listener.local_addr().unwrap();
621 let server = tokio::spawn(async move {
622 let (mut stream, _) = listener.accept().await.unwrap();
623 for _ in 0..2 {
624 let mut request = Vec::new();
625 while !request.ends_with(b"\r\n\r\n") {
626 let mut byte = [0];
627 stream.read_exact(&mut byte).await.unwrap();
628 request.push(byte[0]);
629 }
630 stream
631 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
632 .await
633 .unwrap();
634 }
635 });
636
637 let client = standard_client_builder()
638 .pool_idle_timeout(Duration::from_secs(30))
639 .pool_max_idle_per_host(1)
640 .build();
641 for _ in 0..2 {
642 let response = client
643 .get(format!("http://{address}/"))
644 .unwrap()
645 .send()
646 .await
647 .unwrap();
648 assert_eq!(response.text().await.unwrap(), "ok");
649 }
650
651 server.await.unwrap();
652 }
653}