Skip to main content

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