Skip to main content

ntex/client/
builder.rs

1use urly::Url;
2
3use crate::connect::{self, Connect as TcpConnect, Connector as TcpConnector};
4use crate::service::{Identity, IntoService, Middleware, Pipeline, Service, Stack, apply_fn};
5use crate::{SharedCfg, error::Error, io::IoBoxed};
6
7use super::connector::Connector;
8use super::error::{ClientError, ConnectError};
9use super::pool::ConnectionPool;
10use super::service::{ServiceRequest, ServiceResponse};
11use super::{Client, ClientConfig, Connect, ConnectorPipeline, sender::Sender};
12
13#[cfg(feature = "openssl")]
14use tls_openssl::ssl::SslConnector as OpensslConnector;
15
16#[cfg(feature = "rustls")]
17use tls_rustls::ClientConfig as RustlsClientConfig;
18
19/// An HTTP client builder.
20///
21/// This type configures connectors and middleware before constructing a
22/// [`Client`].
23#[derive(Debug)]
24pub struct ClientBuilder<M = Identity> {
25    middleware: M,
26    svc: ConnectorPipeline,
27    secure_svc: Option<ConnectorPipeline>,
28}
29
30impl Default for ClientBuilder<Identity> {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36impl ClientBuilder<Identity> {
37    #[must_use]
38    /// Creates a client builder with default connectors.
39    pub fn new() -> Self {
40        let svc = ConnectorPipeline::new(
41            apply_fn(TcpConnector::new(), async move |msg: Connect, svc| {
42                svc.call(TcpConnect::new(msg.uri).set_addr(msg.addr)).await
43            })
44            .map(IoBoxed::from)
45            .map_err(|e| e.map(ConnectError::from)),
46        );
47
48        let builder = ClientBuilder {
49            svc,
50            secure_svc: None,
51            middleware: Identity,
52        };
53
54        #[cfg(feature = "openssl")]
55        {
56            use tls_openssl::ssl::SslMethod;
57
58            let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap();
59            let _ = ssl
60                .set_alpn_protos(b"\x02h2\x08http/1.1")
61                .map_err(|e| log::error!("Cannot set ALPN protocol: {e:?}"));
62            ssl.set_verify(tls_openssl::ssl::SslVerifyMode::NONE);
63
64            builder.openssl(ssl.build())
65        }
66        #[cfg(all(not(feature = "openssl"), feature = "rustls"))]
67        {
68            use tls_rustls::RootCertStore;
69
70            let protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
71            let cert_store = webpki_roots::TLS_SERVER_ROOTS
72                .iter()
73                .cloned()
74                .collect::<RootCertStore>();
75            let mut config = RustlsClientConfig::builder()
76                .with_root_certificates(cert_store)
77                .with_no_client_auth();
78            config.alpn_protocols = protos;
79            builder.rustls(config)
80        }
81        #[cfg(not(any(feature = "openssl", feature = "rustls")))]
82        {
83            builder
84        }
85    }
86}
87
88impl<M> ClientBuilder<M> {
89    #[must_use]
90    #[cfg(feature = "openssl")]
91    /// Uses an OpenSSL connector for secure connections.
92    pub fn openssl(self, config: OpensslConnector) -> Self {
93        use crate::connect::openssl::SslConnector;
94
95        self.secure_connector(SslConnector::new(config))
96    }
97
98    #[must_use]
99    #[cfg(feature = "rustls")]
100    /// Uses a rustls connector for secure connections.
101    pub fn rustls(self, config: RustlsClientConfig) -> Self {
102        use crate::connect::rustls::TlsConnector;
103
104        self.secure_connector(TlsConnector::new(config))
105    }
106
107    #[must_use]
108    /// Uses a custom connector for unsecured connections.
109    pub fn connector<T>(mut self, f: impl IntoService<T, SharedCfg, TcpConnect<Url>>) -> Self
110    where
111        T: Service<SharedCfg, TcpConnect<Url>, Error = Error<connect::ConnectError>> + 'static,
112        IoBoxed: From<T::Res>,
113    {
114        self.svc = ConnectorPipeline::new(
115            apply_fn(f.into_service(), async move |msg: Connect, svc| {
116                svc.call(TcpConnect::new(msg.uri).set_addr(msg.addr)).await
117            })
118            .map(IoBoxed::from)
119            .map_err(|e| e.map(ConnectError::from)),
120        );
121        self
122    }
123
124    #[must_use]
125    /// Uses a custom connector for secure connections.
126    pub fn secure_connector<T>(mut self, f: impl IntoService<T, SharedCfg, TcpConnect<Url>>) -> Self
127    where
128        T: Service<SharedCfg, TcpConnect<Url>, Error = Error<connect::ConnectError>> + 'static,
129        IoBoxed: From<T::Res>,
130    {
131        self.secure_svc = Some(ConnectorPipeline::new(
132            apply_fn(f.into_service(), async move |msg: Connect, svc| {
133                svc.call(TcpConnect::new(msg.uri).set_addr(msg.addr)).await
134            })
135            .map(IoBoxed::from)
136            .map_err(|e| e.map(ConnectError::from)),
137        ));
138        self
139    }
140
141    #[must_use]
142    /// Applies middleware to the client request service.
143    ///
144    /// Middleware can inspect or modify every request and response.
145    ///
146    /// ```rust
147    /// use ntex::client::{Client, ServiceRequest};
148    /// use ntex::service::{fn_layer, cfg::SharedCfg};
149    ///
150    /// #[ntex::main]
151    /// async fn main() {
152    ///     let client = Client::builder()
153    ///         .middleware(fn_layer(
154    ///             async move |mut req: ServiceRequest, svc| {
155    ///                 println!("{:?}", req.head().uri);
156    ///                 svc.call(req).await
157    ///             }
158    ///         ))
159    ///         .build(SharedCfg::default());
160    /// }
161    /// ```
162    pub fn middleware<U>(self, mw: U) -> ClientBuilder<Stack<U, M>> {
163        ClientBuilder {
164            middleware: Stack::new(mw, self.middleware),
165            svc: self.svc,
166            secure_svc: self.secure_svc,
167        }
168    }
169
170    /// Builds the [`Client`] using the supplied shared configuration.
171    ///
172    /// Add a [`ClientConfig`] to [`SharedCfg`] to customize client behavior:
173    ///
174    /// ```rust
175    /// use ntex::SharedCfg;
176    /// use ntex::client::{Client, ClientConfig};
177    /// use ntex::time::Millis;
178    ///
179    /// #[ntex::main]
180    /// async fn main() {
181    ///     let cfg = SharedCfg::new("HTTP-CLIENT")
182    ///         .add(
183    ///             ClientConfig::new()
184    ///                 .set_response_timeout(Millis(10_000))
185    ///                 .set_response_payload_limit(1024 * 1024),
186    ///         )
187    ///         .build();
188    ///
189    ///     let _client = Client::builder().build(cfg);
190    /// }
191    /// ```
192    pub fn build(self, cfg: impl Into<SharedCfg>) -> Client
193    where
194        M: Middleware<Sender, SharedCfg>,
195        M::Service: Service<SharedCfg, ServiceRequest, Res = ServiceResponse, Error = Error<ClientError>>
196            + 'static,
197    {
198        let cfg = cfg.into();
199        let config = cfg.get::<ClientConfig>();
200
201        let connector = Connector {
202            tcp_pool: ConnectionPool::new(self.svc, config.clone()),
203            ssl_pool: self
204                .secure_svc
205                .map(|svc| ConnectionPool::new(svc, config.clone())),
206        };
207        let svc = self.middleware.create(&cfg, Sender::new(connector));
208
209        Client::with_service(config, Pipeline::new(cfg, svc))
210    }
211}