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 = crate::net::tls::client::TlsClientConfig::default_http();
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(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 = crate::net::tls::client::TlsClientConfig::default_http();
113
114        EasyHttpConnectorBuilder::new()
115            .with_default_transport_connector()
116            .with_tls_proxy_support_using_rustls()
117            .with_proxy_support()
118            .with_tls_support_using_rustls(tls_config)
119            .with_default_http_connector(exec)
120            .build_client()
121    }
122
123    #[cfg(not(any(feature = "rustls", feature = "boring")))]
124    pub fn default_with_executor(exec: Executor) -> Self {
125        EasyHttpConnectorBuilder::new()
126            .with_default_transport_connector()
127            .without_tls_proxy_support()
128            .with_proxy_support()
129            .without_tls_support()
130            .with_default_http_connector(exec)
131            .build_client()
132    }
133}
134
135impl<BodyIn, ConnResponse> EasyHttpWebClient<BodyIn, ConnResponse, ()>
136where
137    BodyIn: Send + 'static,
138{
139    /// Create a new [`EasyHttpWebClient`] using the provided connector
140    #[must_use]
141    pub fn new<S>(connector: S) -> Self
142    where
143        S: Service<Request<BodyIn>, Output = ConnResponse, Error: Into<BoxError>>,
144    {
145        Self {
146            connector: MapErr::into_opaque_error(connector).boxed(),
147            jit_layers: (),
148        }
149    }
150}
151
152impl<BodyIn, ConnResponse, L> EasyHttpWebClient<BodyIn, ConnResponse, L> {
153    /// Set the connector that this [`EasyHttpWebClient`] will use
154    #[must_use]
155    pub fn with_connector<S, BodyInNew, ConnResponseNew>(
156        self,
157        connector: S,
158    ) -> EasyHttpWebClient<BodyInNew, ConnResponseNew, L>
159    where
160        S: Service<Request<BodyInNew>, Output = ConnResponseNew, Error: Into<BoxError>>,
161        BodyInNew: Send + 'static,
162    {
163        EasyHttpWebClient {
164            connector: MapErr::into_opaque_error(connector).boxed(),
165            jit_layers: self.jit_layers,
166        }
167    }
168
169    /// [`Layer`] which will be applied just in time (JIT) before the request is send, but after
170    /// the connection has been established.
171    ///
172    /// Simplified flow of how the [`EasyHttpWebClient`] works:
173    /// 1. External: let response = client.serve(request)
174    /// 2. Internal: let http_connection = self.connector.serve(request)
175    /// 3. Internal: let response = jit_layers.layer(http_connection).serve(request)
176    pub fn with_jit_layer<T>(self, jit_layers: T) -> EasyHttpWebClient<BodyIn, ConnResponse, T> {
177        EasyHttpWebClient {
178            connector: self.connector,
179            jit_layers,
180        }
181    }
182}
183
184impl<Body, ConnectionBody, Connection, L> Service<Request<Body>>
185    for EasyHttpWebClient<Body, EstablishedClientConnection<Connection, Request<ConnectionBody>>, L>
186where
187    Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
188    Connection:
189        Service<Request<ConnectionBody>, Output = Response, Error = BoxError> + ExtensionsRef,
190    // Body type this connection will be able to send, this is not necessarily the same one that
191    // was used in the request that created this connection
192    ConnectionBody:
193        StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
194    L: Layer<
195            Connection,
196            Service: Service<Request<ConnectionBody>, Output = Response, Error = BoxError>,
197        > + Send
198        + Sync
199        + 'static,
200{
201    type Output = Response;
202    type Error = OpaqueError;
203
204    async fn serve(&self, req: Request<Body>) -> Result<Self::Output, Self::Error> {
205        let uri = req.uri().clone();
206
207        let EstablishedClientConnection {
208            input: req,
209            conn: http_connection,
210        } = self.connector.serve(req).await.into_opaque_error()?;
211
212        req.extensions()
213            .insert(Egress(http_connection.extensions().clone()));
214
215        let http_connection = self.jit_layers.layer(http_connection);
216
217        // NOTE: stack might change request version based on connector data,
218        tracing::trace!(url.full = %uri, "send http req to connector stack");
219
220        let result = http_connection.serve(req).await;
221
222        match result {
223            Ok(resp) => {
224                tracing::trace!(url.full = %uri, "response received from connector stack");
225                Ok(resp)
226            }
227            Err(err) => Err(err
228                .context("http request failure")
229                .context_field("uri", uri)
230                .into_opaque_error()),
231        }
232    }
233}