rama/http/client/
mod.rs

1//! rama http client support
2//!
3//! Contains re-exports from `rama-http-backend::client`
4//! and adds `EasyHttpWebClient`, an opiniated http web client which
5//! supports most common use cases and provides sensible defaults.
6use std::fmt;
7
8use crate::{
9    Service,
10    error::{BoxError, ErrorContext, OpaqueError},
11    http::{Request, Response, StreamingBody},
12    net::client::EstablishedClientConnection,
13    service::BoxService,
14    telemetry::tracing,
15};
16
17#[doc(inline)]
18pub use ::rama_http_backend::client::*;
19
20pub mod builder;
21#[doc(inline)]
22pub use builder::EasyHttpWebClientBuilder;
23
24#[cfg(feature = "socks5")]
25mod proxy_connector;
26#[cfg(feature = "socks5")]
27#[cfg_attr(docsrs, doc(cfg(feature = "socks5")))]
28#[doc(inline)]
29pub use proxy_connector::{MaybeProxiedConnection, ProxyConnector, ProxyConnectorLayer};
30
31/// An opiniated http client that can be used to serve HTTP requests.
32///
33/// Use [`EasyHttpWebClient::builder()`] to easily create a client with
34/// a common Http connector setup (tcp + proxy + tls + http) or bring your
35/// own http connector.
36///
37/// You can fork this http client in case you have use cases not possible with this service example.
38/// E.g. perhaps you wish to have middleware in into outbound requests, after they
39/// passed through your "connector" setup. All this and more is possible by defining your own
40/// http client. Rama is here to empower you, the building blocks are there, go crazy
41/// with your own service fork and use the full power of Rust at your fingertips ;)
42pub struct EasyHttpWebClient<BodyIn, ConnResponse> {
43    connector: BoxService<Request<BodyIn>, ConnResponse, BoxError>,
44}
45
46impl<BodyIn, ConnResponse> fmt::Debug for EasyHttpWebClient<BodyIn, ConnResponse> {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.debug_struct("EasyHttpWebClient").finish()
49    }
50}
51
52impl<BodyIn, ConnResponse> Clone for EasyHttpWebClient<BodyIn, ConnResponse> {
53    fn clone(&self) -> Self {
54        Self {
55            connector: self.connector.clone(),
56        }
57    }
58}
59
60impl EasyHttpWebClient<(), ()> {
61    /// Create a [`EasyHttpWebClientBuilder`] to easily create a [`EasyHttpWebClient`]
62    #[must_use]
63    pub fn builder() -> EasyHttpWebClientBuilder {
64        EasyHttpWebClientBuilder::new()
65    }
66}
67
68impl<Body> Default
69    for EasyHttpWebClient<Body, EstablishedClientConnection<HttpClientService<Body>, Request<Body>>>
70where
71    Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
72{
73    #[cfg(feature = "boring")]
74    fn default() -> Self {
75        let tls_config =
76            rama_tls_boring::client::TlsConnectorDataBuilder::new_http_auto().into_shared_builder();
77
78        EasyHttpWebClientBuilder::new()
79            .with_default_transport_connector()
80            .with_tls_proxy_support_using_boringssl()
81            .with_proxy_support()
82            .with_tls_support_using_boringssl(Some(tls_config))
83            .with_default_http_connector()
84            .build()
85    }
86
87    #[cfg(all(feature = "rustls", not(feature = "boring")))]
88    fn default() -> Self {
89        let tls_config = rama_tls_rustls::client::TlsConnectorData::new_http_auto()
90            .expect("connector data with http auto");
91
92        EasyHttpWebClientBuilder::new()
93            .with_default_transport_connector()
94            .with_tls_proxy_support_using_rustls()
95            .with_proxy_support()
96            .with_tls_support_using_rustls(Some(tls_config))
97            .with_default_http_connector()
98            .build()
99    }
100
101    #[cfg(not(any(feature = "rustls", feature = "boring")))]
102    fn default() -> Self {
103        EasyHttpWebClientBuilder::new()
104            .with_default_transport_connector()
105            .without_tls_proxy_support()
106            .with_proxy_support()
107            .without_tls_support()
108            .with_default_http_connector()
109            .build()
110    }
111}
112
113impl<BodyIn, ConnResponse> EasyHttpWebClient<BodyIn, ConnResponse> {
114    /// Create a new [`EasyHttpWebClient`] using the provided connector
115    #[must_use]
116    pub fn new(connector: BoxService<Request<BodyIn>, ConnResponse, BoxError>) -> Self {
117        Self { connector }
118    }
119
120    /// Set the connector that this [`EasyHttpWebClient`] will use
121    #[must_use]
122    pub fn with_connector<BodyInNew, ConnResponseNew>(
123        self,
124        connector: BoxService<Request<BodyInNew>, ConnResponseNew, BoxError>,
125    ) -> EasyHttpWebClient<BodyInNew, ConnResponseNew> {
126        EasyHttpWebClient { connector }
127    }
128}
129
130impl<Body, ModifiedBody, ConnResponse> Service<Request<Body>>
131    for EasyHttpWebClient<Body, EstablishedClientConnection<ConnResponse, Request<ModifiedBody>>>
132where
133    Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
134    ModifiedBody:
135        StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
136    ConnResponse: Service<Request<ModifiedBody>, Response = Response, Error = BoxError>,
137{
138    type Response = Response;
139
140    type Error = OpaqueError;
141
142    async fn serve(&self, req: Request<Body>) -> Result<Self::Response, Self::Error> {
143        let uri = req.uri().clone();
144
145        let EstablishedClientConnection { req, conn } = self.connector.serve(req).await?;
146        // NOTE: stack might change request version based on connector data,
147        tracing::trace!(url.full = %uri, "send http req to connector stack");
148
149        let result = conn.serve(req).await;
150
151        let resp = result
152            .map_err(OpaqueError::from_boxed)
153            .with_context(|| format!("http request failure for uri: {uri}"))?;
154
155        tracing::trace!(url.full = %uri, "response received from connector stack");
156
157        Ok(resp)
158    }
159}