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