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