Skip to main content

ntex/client/
mod.rs

1//! HTTP client.
2//!
3//! ```rust,no_run
4//! use ntex::client::Client;
5//!
6//! #[ntex::main]
7//! async fn main() {
8//!     let client = Client::new();
9//!
10//!     let response = client
11//!         .get("https://www.rust-lang.org")
12//!         .header("User-Agent", "ntex")
13//!         .send()
14//!         .await;
15//!
16//!     println!("Response: {response:?}");
17//! }
18//! ```
19use std::rc::Rc;
20
21mod builder;
22mod cfg;
23mod codec;
24mod connection;
25mod connector;
26pub mod error;
27mod h1proto;
28mod h2proto;
29mod pool;
30mod request;
31mod response;
32mod sender;
33mod service;
34mod test;
35
36pub use self::builder::ClientBuilder;
37pub use self::cfg::ClientConfig;
38pub use self::request::ClientRequest;
39pub use self::response::{ClientResponse, JsonBody, MessageBody};
40pub use self::service::{ServiceRequest, ServiceResponse};
41pub use self::test::TestResponse;
42
43pub(crate) use self::codec::{ClientCodec, ClientPayloadCodec};
44#[cfg(feature = "ws")]
45pub(crate) use self::h1proto::host_header;
46use urly::Url;
47
48use crate::client::error::{ConnectError, InvalidUrl};
49use crate::http::{HeaderMap, Method, RequestHead, body::BodySize};
50use crate::service::{cfg::SharedCfg, pipeline::PipelineState};
51use crate::{Cfg, Pipeline, error::Error, io::IoBoxed};
52
53type ConnectorPipeline = PipelineState<SharedCfg, Connect, IoBoxed, Error<ConnectError>>;
54
55#[derive(Debug, Clone)]
56pub(crate) struct Connect {
57    pub(crate) uri: Url,
58    pub(crate) addr: Option<std::net::SocketAddr>,
59}
60
61/// An HTTP client.
62///
63/// ```rust,no_run
64/// use ntex::client::Client;
65///
66/// #[ntex::main]
67/// async fn main() {
68///     let client = Client::new();
69///
70///     let response = client
71///         .get("https://www.rust-lang.org")
72///         .header("User-Agent", "ntex")
73///         .send()
74///         .await;
75///
76///     println!("Response: {response:?}");
77/// }
78/// ```
79///
80/// # Shutdown
81///
82/// Clones of a client share its connection pools. A pool is stopped once the
83/// last clone and every request created from it are dropped, and dropping does
84/// not wait for connections to close:
85///
86/// - Requests waiting for a connection fail with
87///   [`ConnectError::Disconnected`].
88/// - Idle HTTP/1 connections are shut down in the background, bounded by the
89///   I/O [shutdown timeout](crate::io::IoConfig::set_shutdown_timeout).
90/// - HTTP/2 connections stop accepting requests and are closed gracefully in
91///   the background once their in-flight requests have completed. Their
92///   closing is not bounded by a timeout.
93/// - Responses that are still being read keep their connections. An HTTP/1
94///   connection is closed instead of being returned to the stopped pool.
95/// - A connection that is still being established is closed once the
96///   connect completes.
97#[derive(Debug, Clone)]
98pub struct Client {
99    cfg: Cfg<ClientConfig>,
100    svc: Rc<Pipeline<ServiceRequest, ServiceResponse, Error<error::ClientError>>>,
101}
102
103impl Default for Client {
104    fn default() -> Self {
105        Client::new()
106    }
107}
108
109impl Client {
110    /// Creates a client with default settings.
111    pub fn new() -> Client {
112        ClientBuilder::new().build(SharedCfg::default())
113    }
114
115    /// Creates a client builder.
116    pub fn builder() -> ClientBuilder {
117        ClientBuilder::new()
118    }
119
120    /// Creates a client with shared service configuration.
121    pub fn with_config(cfg: impl Into<SharedCfg>) -> Client {
122        ClientBuilder::new().build(cfg.into())
123    }
124
125    pub(crate) fn with_service(
126        cfg: Cfg<ClientConfig>,
127        svc: Pipeline<ServiceRequest, ServiceResponse, Error<error::ClientError>>,
128    ) -> Self {
129        Client {
130            cfg,
131            svc: Rc::new(svc),
132        }
133    }
134
135    /// Waits until the client is ready to process requests.
136    pub async fn ready(&self) -> Result<(), Error<error::ClientError>> {
137        self.svc.ready().await
138    }
139
140    /// Creates an HTTP request with the specified method and URL.
141    pub fn request<U>(&self, method: Method, url: U) -> ClientRequest
142    where
143        Url: TryFrom<U>,
144        <Url as TryFrom<U>>::Error: Into<InvalidUrl>,
145    {
146        let mut req = ClientRequest::new(method, url, self.cfg.clone(), self.svc.bind());
147        for (key, value) in self.cfg.headers() {
148            req = req.set_header_if_none(key.clone(), value.clone());
149        }
150        req
151    }
152
153    /// Creates a [`ClientRequest`] from a [`RequestHead`].
154    ///
155    /// This is useful for proxy requests. The method and headers are copied
156    /// from `head`; existing client default headers are not overwritten.
157    pub fn request_from<U>(&self, url: U, head: &RequestHead) -> ClientRequest
158    where
159        Url: TryFrom<U>,
160        <Url as TryFrom<U>>::Error: Into<InvalidUrl>,
161    {
162        let mut req = self.request(head.method.clone(), url);
163        for (key, value) in &head.headers {
164            // every value of a multi-value header is copied
165            if !self.cfg.headers().contains_key(key) {
166                req.headers_mut().append(key.clone(), value.clone());
167            }
168        }
169        req
170    }
171
172    /// Construct HTTP *GET* request.
173    pub fn get<U>(&self, url: U) -> ClientRequest
174    where
175        Url: TryFrom<U>,
176        <Url as TryFrom<U>>::Error: Into<InvalidUrl>,
177    {
178        self.request(Method::GET, url)
179    }
180
181    /// Construct HTTP *HEAD* request.
182    pub fn head<U>(&self, url: U) -> ClientRequest
183    where
184        Url: TryFrom<U>,
185        <Url as TryFrom<U>>::Error: Into<InvalidUrl>,
186    {
187        self.request(Method::HEAD, url)
188    }
189
190    /// Construct HTTP *PUT* request.
191    pub fn put<U>(&self, url: U) -> ClientRequest
192    where
193        Url: TryFrom<U>,
194        <Url as TryFrom<U>>::Error: Into<InvalidUrl>,
195    {
196        self.request(Method::PUT, url)
197    }
198
199    /// Construct HTTP *POST* request.
200    pub fn post<U>(&self, url: U) -> ClientRequest
201    where
202        Url: TryFrom<U>,
203        <Url as TryFrom<U>>::Error: Into<InvalidUrl>,
204    {
205        self.request(Method::POST, url)
206    }
207
208    /// Construct HTTP *PATCH* request.
209    pub fn patch<U>(&self, url: U) -> ClientRequest
210    where
211        Url: TryFrom<U>,
212        <Url as TryFrom<U>>::Error: Into<InvalidUrl>,
213    {
214        self.request(Method::PATCH, url)
215    }
216
217    /// Construct HTTP *DELETE* request.
218    pub fn delete<U>(&self, url: U) -> ClientRequest
219    where
220        Url: TryFrom<U>,
221        <Url as TryFrom<U>>::Error: Into<InvalidUrl>,
222    {
223        self.request(Method::DELETE, url)
224    }
225
226    /// Construct HTTP *QUERY* request.
227    pub fn query<U>(&self, url: U) -> ClientRequest
228    where
229        Url: TryFrom<U>,
230        <Url as TryFrom<U>>::Error: Into<InvalidUrl>,
231    {
232        self.request(Method::QUERY, url)
233    }
234
235    /// Construct HTTP *OPTIONS* request.
236    pub fn options<U>(&self, url: U) -> ClientRequest
237    where
238        Url: TryFrom<U>,
239        <Url as TryFrom<U>>::Error: Into<InvalidUrl>,
240    {
241        self.request(Method::OPTIONS, url)
242    }
243}
244
245#[derive(Debug)]
246pub(crate) struct ClientRawRequest {
247    pub(crate) head: crate::http::Message<RequestHead>,
248    pub(crate) headers: Option<HeaderMap>,
249    pub(crate) size: BodySize,
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::http::header::{self, HeaderValue};
256
257    #[crate::rt_test]
258    async fn request_from_head() {
259        let client = Client::builder().build(
260            SharedCfg::new("TEST").add(ClientConfig::new().set_header("x-default", "cfg").unwrap()),
261        );
262        let mut head = RequestHead {
263            method: Method::PATCH,
264            ..Default::default()
265        };
266        head.headers
267            .insert(header::ACCEPT, HeaderValue::from_static("text/html"));
268        head.headers
269            .append(header::ACCEPT, HeaderValue::from_static("text/plain"));
270        head.headers.insert(
271            "x-default".try_into().unwrap(),
272            HeaderValue::from_static("head"),
273        );
274
275        let req = client.request_from("http://localhost/", &head);
276        assert_eq!(req.get_method(), Method::PATCH);
277        let accept: Vec<_> = req.headers().get_all(header::ACCEPT).collect();
278        assert_eq!(accept, ["text/html", "text/plain"]);
279        // client default headers are not overwritten
280        let default: Vec<_> = req.headers().get_all("x-default").collect();
281        assert_eq!(default, ["cfg"]);
282
283        let req = Client::default().request_from("http://localhost/", &head);
284        assert_eq!(req.headers().get("x-default").unwrap(), "head");
285    }
286}