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    Context, Service,
10    error::{BoxError, ErrorContext, OpaqueError},
11    http::{Request, Response, dep::http_body},
12    net::client::EstablishedClientConnection,
13    service::BoxService,
14    telemetry::tracing,
15};
16
17#[doc(inline)]
18pub use ::rama_http_backend::client::*;
19
20pub mod builder;
21#[doc(inline)]
22pub use builder::EasyHttpWebClientBuilder;
23
24#[cfg(feature = "socks5")]
25mod proxy_connector;
26#[cfg(feature = "socks5")]
27#[doc(inline)]
28pub use proxy_connector::{MaybeProxiedConnection, ProxyConnector, ProxyConnectorLayer};
29
30/// An opiniated http client that can be used to serve HTTP requests.
31///
32/// Use [`EasyHttpWebClient::builder()`] to easily create a client with
33/// a common Http connector setup (tcp + proxy + tls + http) or bring your
34/// own http connector.
35///
36/// You can fork this http client in case you have use cases not possible with this service example.
37/// E.g. perhaps you wish to have middleware in into outbound requests, after they
38/// passed through your "connector" setup. All this and more is possible by defining your own
39/// http client. Rama is here to empower you, the building blocks are there, go crazy
40/// with your own service fork and use the full power of Rust at your fingertips ;)
41pub struct EasyHttpWebClient<BodyIn, ConnResponse> {
42    connector: BoxService<Request<BodyIn>, ConnResponse, BoxError>,
43}
44
45impl<BodyIn, ConnResponse> fmt::Debug for EasyHttpWebClient<BodyIn, ConnResponse> {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        f.debug_struct("EasyHttpWebClient").finish()
48    }
49}
50
51impl<BodyIn, ConnResponse> Clone for EasyHttpWebClient<BodyIn, ConnResponse> {
52    fn clone(&self) -> Self {
53        Self {
54            connector: self.connector.clone(),
55        }
56    }
57}
58
59impl EasyHttpWebClient<(), ()> {
60    /// Create a [`EasyHttpWebClientBuilder`] to easily create a [`EasyHttpWebClient`]
61    #[must_use]
62    pub fn builder() -> EasyHttpWebClientBuilder {
63        EasyHttpWebClientBuilder::new()
64    }
65}
66
67impl<Body> Default
68    for EasyHttpWebClient<Body, EstablishedClientConnection<HttpClientService<Body>, Request<Body>>>
69where
70    Body: http_body::Body<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
71{
72    #[cfg(feature = "boring")]
73    fn default() -> Self {
74        let tls_config =
75            rama_tls_boring::client::TlsConnectorDataBuilder::new_http_auto().into_shared_builder();
76
77        EasyHttpWebClientBuilder::new()
78            .with_default_transport_connector()
79            .with_tls_proxy_support_using_boringssl()
80            .with_proxy_support()
81            .with_tls_support_using_boringssl(Some(tls_config))
82            .build()
83    }
84
85    #[cfg(all(feature = "rustls", not(feature = "boring")))]
86    fn default() -> Self {
87        let tls_config = rama_tls_rustls::client::TlsConnectorData::new_http_auto()
88            .expect("connector data with http auto");
89
90        EasyHttpWebClientBuilder::new()
91            .with_default_transport_connector()
92            .with_tls_proxy_support_using_rustls()
93            .with_proxy_support()
94            .with_tls_support_using_rustls(Some(tls_config))
95            .build()
96    }
97
98    #[cfg(not(any(feature = "rustls", feature = "boring")))]
99    fn default() -> Self {
100        EasyHttpWebClientBuilder::new()
101            .with_default_transport_connector()
102            .without_tls_proxy_support()
103            .with_proxy_support()
104            .without_tls_support()
105            .build()
106    }
107}
108
109impl<BodyIn, ConnResponse> EasyHttpWebClient<BodyIn, ConnResponse> {
110    /// Create a new [`EasyHttpWebClient`] using the provided connector
111    #[must_use]
112    pub fn new(connector: BoxService<Request<BodyIn>, ConnResponse, BoxError>) -> Self {
113        Self { connector }
114    }
115
116    /// Set the connector that this [`EasyHttpWebClient`] will use
117    #[must_use]
118    pub fn with_connector<BodyInNew, ConnResponseNew>(
119        self,
120        connector: BoxService<Request<BodyInNew>, ConnResponseNew, BoxError>,
121    ) -> EasyHttpWebClient<BodyInNew, ConnResponseNew> {
122        EasyHttpWebClient { connector }
123    }
124}
125
126impl<Body, ModifiedBody, ConnResponse> Service<Request<Body>>
127    for EasyHttpWebClient<Body, EstablishedClientConnection<ConnResponse, Request<ModifiedBody>>>
128where
129    Body: http_body::Body<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
130    ModifiedBody:
131        http_body::Body<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
132    ConnResponse: Service<Request<ModifiedBody>, Response = Response, Error = BoxError>,
133{
134    type Response = Response;
135
136    type Error = OpaqueError;
137
138    async fn serve(&self, ctx: Context, req: Request<Body>) -> Result<Self::Response, Self::Error> {
139        let uri = req.uri().clone();
140
141        let EstablishedClientConnection { ctx, req, conn } = self.connector.serve(ctx, req).await?;
142        // NOTE: stack might change request version based on connector data,
143        tracing::trace!(url.full = %uri, "send http req to connector stack");
144
145        let result = conn.serve(ctx, req).await;
146
147        let resp = result
148            .map_err(OpaqueError::from_boxed)
149            .with_context(|| format!("http request failure for uri: {uri}"))?;
150
151        tracing::trace!(url.full = %uri, "response received from connector stack");
152
153        Ok(resp)
154    }
155}