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    Layer, Service,
10    error::{BoxError, ErrorContext, OpaqueError},
11    extensions::{ExtensionsMut, ExtensionsRef},
12    http::{Request, Response, StreamingBody},
13    net::client::EstablishedClientConnection,
14    service::BoxService,
15    telemetry::tracing,
16};
17
18#[doc(inline)]
19pub use ::rama_http_backend::client::*;
20
21pub mod builder;
22#[doc(inline)]
23pub use builder::EasyHttpConnectorBuilder;
24
25#[cfg(feature = "socks5")]
26mod proxy_connector;
27#[cfg(feature = "socks5")]
28#[cfg_attr(docsrs, doc(cfg(feature = "socks5")))]
29#[doc(inline)]
30pub use proxy_connector::{MaybeProxiedConnection, ProxyConnector, ProxyConnectorLayer};
31
32/// An opiniated http client that can be used to serve HTTP requests.
33///
34/// Use [`EasyHttpWebClient::connector_builder()`] to easily create a client with
35/// a common Http connector setup (tcp + proxy + tls + http) or bring your
36/// own http connector.
37///
38/// You can fork this http client in case you have use cases not possible with this service example.
39/// E.g. perhaps you wish to have middleware in into outbound requests, after they
40/// passed through your "connector" setup. All this and more is possible by defining your own
41/// http client. Rama is here to empower you, the building blocks are there, go crazy
42/// with your own service fork and use the full power of Rust at your fingertips ;)
43pub struct EasyHttpWebClient<BodyIn, ConnResponse, L> {
44    connector: BoxService<Request<BodyIn>, ConnResponse, BoxError>,
45    jit_layers: L,
46}
47
48impl<BodyIn, ConnResponse, L> fmt::Debug for EasyHttpWebClient<BodyIn, ConnResponse, L> {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        f.debug_struct("EasyHttpWebClient").finish()
51    }
52}
53
54impl<BodyIn, ConnResponse, L: Clone> Clone for EasyHttpWebClient<BodyIn, ConnResponse, L> {
55    fn clone(&self) -> Self {
56        Self {
57            connector: self.connector.clone(),
58            jit_layers: self.jit_layers.clone(),
59        }
60    }
61}
62
63impl EasyHttpWebClient<(), (), ()> {
64    /// Create a [`EasyHttpConnectorBuilder`] to easily create a [`EasyHttpWebClient`] with a custom connector
65    #[must_use]
66    pub fn connector_builder() -> EasyHttpConnectorBuilder {
67        EasyHttpConnectorBuilder::new()
68    }
69}
70
71impl<Body> Default
72    for EasyHttpWebClient<
73        Body,
74        EstablishedClientConnection<HttpClientService<Body>, Request<Body>>,
75        (),
76    >
77where
78    Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
79{
80    #[cfg(feature = "boring")]
81    fn default() -> Self {
82        let tls_config =
83            rama_tls_boring::client::TlsConnectorDataBuilder::new_http_auto().into_shared_builder();
84
85        EasyHttpConnectorBuilder::new()
86            .with_default_transport_connector()
87            .with_tls_proxy_support_using_boringssl()
88            .with_proxy_support()
89            .with_tls_support_using_boringssl(Some(tls_config))
90            .with_default_http_connector()
91            .build_client()
92    }
93
94    #[cfg(all(feature = "rustls", not(feature = "boring")))]
95    fn default() -> Self {
96        let tls_config = rama_tls_rustls::client::TlsConnectorData::try_new_http_auto()
97            .expect("connector data with http auto");
98
99        EasyHttpConnectorBuilder::new()
100            .with_default_transport_connector()
101            .with_tls_proxy_support_using_rustls()
102            .with_proxy_support()
103            .with_tls_support_using_rustls(Some(tls_config))
104            .with_default_http_connector()
105            .build_client()
106    }
107
108    #[cfg(not(any(feature = "rustls", feature = "boring")))]
109    fn default() -> Self {
110        EasyHttpConnectorBuilder::new()
111            .with_default_transport_connector()
112            .without_tls_proxy_support()
113            .with_proxy_support()
114            .without_tls_support()
115            .with_default_http_connector()
116            .build_client()
117    }
118}
119
120impl<BodyIn, ConnResponse> EasyHttpWebClient<BodyIn, ConnResponse, ()> {
121    /// Create a new [`EasyHttpWebClient`] using the provided connector
122    #[must_use]
123    pub fn new(connector: BoxService<Request<BodyIn>, ConnResponse, BoxError>) -> Self {
124        Self {
125            connector,
126            jit_layers: (),
127        }
128    }
129}
130
131impl<BodyIn, ConnResponse, L> EasyHttpWebClient<BodyIn, ConnResponse, L> {
132    /// Set the connector that this [`EasyHttpWebClient`] will use
133    #[must_use]
134    pub fn with_connector<BodyInNew, ConnResponseNew>(
135        self,
136        connector: BoxService<Request<BodyInNew>, ConnResponseNew, BoxError>,
137    ) -> EasyHttpWebClient<BodyInNew, ConnResponseNew, L> {
138        EasyHttpWebClient {
139            connector,
140            jit_layers: self.jit_layers,
141        }
142    }
143
144    /// [`Layer`] which will be applied just in time (JIT) before the request is send, but after
145    /// the connection has been established.
146    ///
147    /// Simplified flow of how the [`EasyHttpWebClient`] works:
148    /// 1. External: let response = client.serve(request)
149    /// 2. Internal: let http_connection = self.connector.serve(request)
150    /// 3. Internal: let response = jit_layers.layer(http_connection).serve(request)
151    pub fn with_jit_layer<T>(self, jit_layers: T) -> EasyHttpWebClient<BodyIn, ConnResponse, T> {
152        EasyHttpWebClient {
153            connector: self.connector,
154            jit_layers,
155        }
156    }
157}
158
159impl<Body, ConnectionBody, Connection, L> Service<Request<Body>>
160    for EasyHttpWebClient<Body, EstablishedClientConnection<Connection, Request<ConnectionBody>>, L>
161where
162    Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
163    Connection:
164        Service<Request<ConnectionBody>, Output = Response, Error = BoxError> + ExtensionsRef,
165    // Body type this connection will be able to send, this is not necessarily the same one that
166    // was used in the request that created this connection
167    ConnectionBody:
168        StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
169    L: Layer<
170            Connection,
171            Service: Service<Request<ConnectionBody>, Output = Response, Error = BoxError>,
172        > + Send
173        + Sync
174        + 'static,
175{
176    type Output = Response;
177    type Error = OpaqueError;
178
179    async fn serve(&self, req: Request<Body>) -> Result<Self::Output, Self::Error> {
180        let uri = req.uri().clone();
181
182        let EstablishedClientConnection {
183            input: mut req,
184            conn: http_connection,
185        } = self.connector.serve(req).await?;
186
187        req.extensions_mut()
188            .extend(http_connection.extensions().clone());
189
190        let http_connection = self.jit_layers.layer(http_connection);
191
192        // NOTE: stack might change request version based on connector data,
193        tracing::trace!(url.full = %uri, "send http req to connector stack");
194
195        let result = http_connection.serve(req).await;
196
197        let resp = result
198            .map_err(OpaqueError::from_boxed)
199            .with_context(|| format!("http request failure for uri: {uri}"))?;
200
201        tracing::trace!(url.full = %uri, "response received from connector stack");
202
203        Ok(resp)
204    }
205}