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