1use 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#[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 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 pub fn with_connector<C: NetworkConnector>(connector: C) -> Self {
66 ClientBuilder::default().build(connector)
67 }
68
69 pub async fn send(&self, request: Request<RequestBody>) -> Result<Response, Error> {
72 Ok(self.inner.request(request).await?)
73 }
74
75 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#[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 pub fn from_hyper_client_builder(inner: HyperClientBuilder) -> Self {
125 Self(inner)
126 }
127
128 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 pub fn pool_idle_timeout(&mut self, val: Option<Duration>) -> &mut Self {
142 self.0.pool_idle_timeout(val);
143 self
144 }
145
146 pub fn http2_only(&mut self, val: bool) -> &mut Self {
152 self.0.http2_only(val);
153 self
154 }
155
156 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 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 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
229pub struct RequestBuilder<'a> {
240 client: &'a Client,
241 details: RequestDetails,
242}
243
244impl<'a> RequestBuilder<'a> {
245 pub fn body<B: Into<RequestBody>>(mut self, body: B) -> Self {
247 self.details.body = Some(body.into());
248 self
249 }
250
251 pub fn headers(mut self, headers: HeaderMap) -> Self {
253 self.details.headers = headers;
254 self
255 }
256
257 pub fn header<H: Header>(mut self, header: H) -> Self {
261 self.details.headers.typed_insert(header);
262 self
263 }
264
265 pub fn build(self) -> Result<Request<RequestBody>, Error> {
270 self.details.into_request()
271 }
272
273 pub async fn send(self) -> Result<Response, Error> {
278 self.details.send(&self.client).await
279 }
280}
281
282#[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 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 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}