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