Skip to main content

simple_hyper_client/
async_client.rs

1/* Copyright (c) Fortanix, Inc.
2 *
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7use crate::body::RequestBody;
8use crate::connector::{ConnectorAdapter, NetworkConnector};
9use crate::error::Error;
10use crate::{HyperClient, HyperClientBuilder, Response};
11
12use headers::{Header, HeaderMap, HeaderMapExt};
13use hyper::body::Body;
14use hyper::{Method, Request, Uri};
15
16use std::convert::{TryFrom, TryInto};
17use std::fmt;
18use std::future::Future;
19use std::sync::Arc;
20use std::time::Duration;
21
22/// A wrapper for [hyper's `Client` type] providing a simpler interface
23///
24/// Example usage:
25/// ```ignore
26/// let connector = HttpConnector::new();
27/// let client = Client::with_connector(connector);
28/// let response = client.get("http://example.com/")?.send().await?;
29/// ```
30///
31/// [hyper's `Client` type]: https://docs.rs/hyper-util/latest/hyper_util/client/legacy/struct.Client.html
32#[derive(Clone)]
33pub struct Client {
34    inner: Arc<HyperClient<ConnectorAdapter, RequestBody>>,
35}
36
37macro_rules! define_method_fn {
38    (@internal $name:ident, $method:ident, $method_str:expr) => {
39        #[doc = "Initiate a "]
40        #[doc = $method_str]
41        #[doc = " request with the specified URI."]
42        ///
43        /// Returns an error if `uri` is invalid.
44        pub fn $name<U>(&self, uri: U) -> Result<RequestBuilder<'_>, Error>
45        where
46            Uri: TryFrom<U>,
47            <Uri as TryFrom<U>>::Error: Into<http::Error>,
48        {
49            self.request(Method::$method, uri)
50        }
51    };
52
53    ($name:ident, $method:ident) => {
54        define_method_fn!(@internal $name, $method, stringify!($method));
55    };
56}
57
58impl Client {
59    pub fn builder() -> ClientBuilder {
60        ClientBuilder::default()
61    }
62
63    /// Create a new `Client` using the specified connector.
64    pub fn with_connector<C: NetworkConnector>(connector: C) -> Self {
65        ClientBuilder::default().build(connector)
66    }
67
68    /// This method can be used instead of [Client::request]
69    /// if the caller already has a [Request].
70    pub async fn send(&self, request: Request<RequestBody>) -> Result<Response, Error> {
71        Ok(self.inner.request(request).await?)
72    }
73
74    /// Initiate a request with the specified method and URI.
75    ///
76    /// Returns an error if `uri` is invalid.
77    pub fn request<U>(&self, method: Method, uri: U) -> Result<RequestBuilder<'_>, Error>
78    where
79        Uri: TryFrom<U>,
80        <Uri as TryFrom<U>>::Error: Into<http::Error>,
81    {
82        let uri = uri.try_into().map_err(Into::into).map_err(Error::Http)?;
83        Ok(RequestBuilder {
84            client: self,
85            details: RequestDetails::new(method, uri),
86        })
87    }
88
89    define_method_fn!(get, GET);
90    define_method_fn!(head, HEAD);
91    define_method_fn!(post, POST);
92    define_method_fn!(patch, PATCH);
93    define_method_fn!(put, PUT);
94    define_method_fn!(delete, DELETE);
95}
96
97// NOTE: the default values are taken from https://docs.rs/hyper-util/latest/hyper_util/client/legacy/struct.Builder.html
98// NOTE: not all configurable aspects of hyper Client are exposed here.
99/// A builder for [`Client`]
100///
101/// [`Client`]: struct.Client.html
102#[derive(Clone)]
103pub struct ClientBuilder(HyperClientBuilder);
104
105impl Default for ClientBuilder {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111impl ClientBuilder {
112    pub fn new() -> Self {
113        Self::from_hyper_client_builder(HyperClientBuilder::new(TokioExecutor))
114    }
115
116    /// Create a builder with a configured instance of [`HyperClientBuilder`].
117    pub fn from_hyper_client_builder(inner: HyperClientBuilder) -> Self {
118        Self(inner)
119    }
120
121    /// Sets the maximum idle connection per host allowed in the pool.
122    ///
123    /// Default is usize::MAX (no limit).
124    pub fn pool_max_idle_per_host(&mut self, max_idle: usize) -> &mut Self {
125        self.0.pool_max_idle_per_host(max_idle);
126        self
127    }
128
129    /// Set an optional timeout for idle sockets being kept-alive.
130    ///
131    /// Pass `None` to disable timeout.
132    ///
133    /// Default is 90 seconds.
134    pub fn pool_idle_timeout(&mut self, val: Option<Duration>) -> &mut Self {
135        self.0.pool_idle_timeout(val);
136        self
137    }
138
139    /// Set whether the connection **must** use HTTP/2.
140    ///
141    /// Note that setting this to true prevents HTTP/1 from being allowed.
142    ///
143    /// Default is false.
144    pub fn http2_only(&mut self, val: bool) -> &mut Self {
145        self.0.http2_only(val);
146        self
147    }
148
149    /// Combine the configuration of this builder with a connector to create a
150    /// `Client`.
151    pub fn build<C: NetworkConnector>(&self, connector: C) -> Client {
152        Client {
153            inner: Arc::new(self.0.build(ConnectorAdapter::new(connector))),
154        }
155    }
156}
157
158pub(crate) struct RequestDetails {
159    pub(crate) method: Method,
160    pub(crate) uri: Uri,
161    pub(crate) headers: HeaderMap,
162    pub(crate) body: Option<RequestBody>,
163}
164
165impl fmt::Debug for RequestDetails {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        f.debug_struct("RequestDetails")
168            .field("method", &self.method)
169            .field("uri", &self.uri)
170            .field("headers", &self.headers.len())
171            .field("body", &self.body.as_ref().map_or("None", |_| "Some(...)"))
172            .finish()
173    }
174}
175
176impl RequestDetails {
177    pub fn new(method: Method, uri: Uri) -> Self {
178        RequestDetails {
179            method,
180            uri,
181            headers: HeaderMap::new(),
182            body: None,
183        }
184    }
185
186    pub async fn send(self, client: &Client) -> Result<Response, Error> {
187        let req = self.into_request()?;
188        Ok(client.inner.request(req).await?)
189    }
190
191    pub fn into_request(self) -> Result<Request<RequestBody>, Error> {
192        let can_have_body = match self.method {
193            // See RFC 7231 section 4.3
194            Method::GET | Method::HEAD | Method::DELETE => false,
195            _ => true,
196        };
197        let body = if can_have_body {
198            self.body.unwrap_or_else(|| RequestBody::empty())
199        } else if self.body.is_some_and(|body| body.size_hint().lower() > 0) {
200            return Err(Error::BodyNotAllowed(self.method));
201        } else {
202            RequestBody::empty()
203        };
204        let mut req = Request::builder().method(self.method).uri(self.uri);
205        match req.headers_mut() {
206            Some(headers) => {
207                *headers = self.headers;
208            }
209            // There is an error in req, but the only way to extract the error is through `req.body()`
210            None => match req.body(RequestBody::empty()) {
211                Err(e) => return Err(e.into()),
212                Ok(_) => {
213                    panic!("request builder must have errors if `fn headers_mut()` returns None")
214                }
215            },
216        }
217
218        Ok(req.body(body)?)
219    }
220}
221
222/// An HTTP request builder
223///
224/// This is created through [`Client::get()`], [`Client::post()`] etc.
225/// You need to call [`send()`] to actually send the request over the network.
226/// If you don't want to send it and just want the resultant [Request], you
227/// can call [RequestBuilder::build].
228///
229/// [`Client::get()`]: struct.Client.html#method.get
230/// [`Client::post()`]: struct.Client.html#method.post
231/// [`send()`]: struct.RequestBuilder.html#method.send
232pub struct RequestBuilder<'a> {
233    client: &'a Client,
234    details: RequestDetails,
235}
236
237impl<'a> RequestBuilder<'a> {
238    /// Set the request body.
239    pub fn body<B: Into<RequestBody>>(mut self, body: B) -> Self {
240        self.details.body = Some(body.into());
241        self
242    }
243
244    /// Set the request headers.
245    pub fn headers(mut self, headers: HeaderMap) -> Self {
246        self.details.headers = headers;
247        self
248    }
249
250    /// Set a single header using [`HeaderMapExt::typed_insert()`].
251    ///
252    /// [`HeaderMapExt::typed_insert()`]: https://docs.rs/headers/0.3.5/headers/trait.HeaderMapExt.html#tymethod.typed_insert
253    pub fn header<H: Header>(mut self, header: H) -> Self {
254        self.details.headers.typed_insert(header);
255        self
256    }
257
258    /// Get the resultant [Request].
259    ///
260    /// Prefer [RequestBuilder::send] unless you have a specific
261    /// need to get the resultant [Request].
262    pub fn build(self) -> Result<Request<RequestBody>, Error> {
263        self.details.into_request()
264    }
265
266    /// Send the request over the network.
267    ///
268    /// Returns an error before sending the request if there is something wrong
269    /// with the request parameters (method, uri, etc.).
270    pub async fn send(self) -> Result<Response, Error> {
271        self.details.send(&self.client).await
272    }
273}
274
275/// The default executor to use with the native hyper client.
276///
277/// Based on the [`tokio`] runtime.
278#[derive(Copy, Clone)]
279pub struct TokioExecutor;
280
281impl<F> hyper::rt::Executor<F> for TokioExecutor
282where
283    F: Future + Send + 'static,
284    F::Output: Send + 'static,
285{
286    fn execute(&self, fut: F) {
287        tokio::spawn(fut);
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use crate::connector::HttpConnector;
295    use crate::util::to_bytes;
296
297    use headers::{ContentLength, ContentType};
298    use hyper::body::Bytes;
299    use hyper::StatusCode;
300    use test_case::test_case;
301    use tokio::io::{AsyncReadExt, AsyncWriteExt};
302    use tokio::net::TcpListener;
303    use tokio::sync::oneshot;
304
305    use std::net::SocketAddr;
306
307    const RESPONSE_OK: &str = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!\r\n";
308    const RESPONSE_404: &str =
309        "HTTP/1.1 404 Not Found\r\nContent-Length: 23\r\n\r\nResource was not found.\r\n";
310
311    async fn test_http_server(resp: &'static str, body_tx: oneshot::Sender<Vec<u8>>) -> SocketAddr {
312        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
313        let addr = listener.local_addr().unwrap();
314        tokio::spawn(async move {
315            let (mut stream, _) = listener.accept().await.unwrap();
316            let mut input = Vec::new();
317            stream.write_all(resp.as_bytes()).await.unwrap();
318            stream.read_to_end(&mut input).await.unwrap();
319            let _ = body_tx.send(input);
320        });
321        addr
322    }
323
324    #[tokio::test]
325    async fn http_client() {
326        let (tx, rx) = oneshot::channel();
327        let addr = test_http_server(RESPONSE_OK, tx).await;
328        let url = format!("http://{}/", addr);
329
330        let connector = HttpConnector::new();
331        let client = Client::with_connector(connector);
332        let response = client
333            .post(url)
334            .unwrap()
335            .header(ContentType::json())
336            .body(r#"{"key":"value"}"#)
337            .send()
338            .await
339            .unwrap();
340
341        // Parse the request received by the server
342        let mut headers = [httparse::EMPTY_HEADER; 64];
343        let mut request = httparse::Request::new(&mut headers);
344        let req_buf = rx.await.unwrap();
345        let body_idx = request.parse(&req_buf).unwrap().unwrap();
346        assert_eq!(request.method, Some("POST"));
347        assert_eq!(request.path, Some("/"));
348        assert_eq!(request.version, Some(1));
349        let content_length = request
350            .headers
351            .iter()
352            .find(|header| header.name == ContentLength::name())
353            .unwrap();
354        assert_eq!(content_length.value, "15".as_bytes());
355        assert_eq!(
356            str::from_utf8(&req_buf[body_idx..]).unwrap(),
357            "{\"key\":\"value\"}"
358        );
359
360        assert_eq!(response.status(), StatusCode::OK);
361        let response_body = to_bytes(response).await.unwrap();
362        assert_eq!(response_body, "Hello, world!".as_bytes());
363    }
364
365    #[test_case(Some(r#"{"key":"value"}"#.into()), false; "non-empty body not allowed")]
366    #[test_case(Some("".into()), true; "empty body allowed")]
367    #[test_case(None, true; "without body allowed")]
368    #[tokio::test]
369    async fn get_request(body: Option<RequestBody>, expect_ok: bool) {
370        let (tx, _rx) = oneshot::channel();
371        let addr = test_http_server(RESPONSE_OK, tx).await;
372        let url = format!("http://{}/", addr);
373
374        let connector = HttpConnector::new();
375        let client = Client::with_connector(connector);
376        let mut builder = client.get(url).unwrap();
377
378        if let Some(body) = body {
379            builder = builder.header(ContentType::json()).body(body);
380        }
381
382        let result = builder.send().await;
383        if expect_ok {
384            result.unwrap();
385        } else {
386            assert_eq!(result.unwrap_err().unwrap_body_not_allowed(), Method::GET);
387        }
388    }
389
390    #[tokio::test]
391    async fn drop_client_before_response() {
392        let (tx, _rx) = oneshot::channel();
393        let addr = test_http_server(RESPONSE_404, tx).await;
394        let url = format!("http://{}/", addr);
395
396        let connector = HttpConnector::new();
397        let client = Client::with_connector(connector);
398        let response = client.get(url).unwrap().send().await.unwrap();
399        drop(client);
400
401        assert_eq!(response.status(), StatusCode::NOT_FOUND);
402        assert_eq!(response.headers().len(), 1);
403        let body = to_bytes(response).await.unwrap();
404        assert_eq!(body, "Resource was not found.");
405    }
406
407    #[tokio::test]
408    async fn http_connector_connect_timeout() {
409        // IP address chosen from 192.0.2.0/24 block defined in RFC 5737.
410        let url = "http://192.0.2.1/";
411        let connector = HttpConnector::new().connect_timeout(Some(Duration::from_millis(100)));
412        let client = Client::with_connector(connector);
413        let err = client.get(url).unwrap().send().await.unwrap_err();
414        assert_eq!(
415            err.to_string(),
416            "client error (Connect): I/O error: connection timed out"
417        );
418    }
419
420    #[test]
421    fn wrapped_body_size_hint() {
422        let body = http_body_util::Full::new(Bytes::from_static(r#"{"key":"value"}"#.as_bytes()));
423        let unwrapped_size_hint = body.size_hint();
424
425        let request = Client::with_connector(HttpConnector::new())
426            .post("https://localhost/")
427            .unwrap()
428            .header(ContentType::json())
429            .body(RequestBody::wrap(body))
430            .build()
431            .unwrap();
432
433        assert_eq!(
434            request.body().size_hint().lower(),
435            unwrapped_size_hint.lower()
436        );
437        assert_eq!(
438            request.body().size_hint().upper(),
439            unwrapped_size_hint.upper()
440        );
441    }
442}