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};
15
16use std::convert::{TryFrom, TryInto};
17use std::fmt;
18use std::future::Future;
19use std::sync::Arc;
20use std::time::Duration;
21
22#[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 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 pub fn with_connector<C: NetworkConnector>(connector: C) -> Self {
65 ClientBuilder::default().build(connector)
66 }
67
68 pub async fn send(&self, request: Request<RequestBody>) -> Result<Response, Error> {
71 Ok(self.inner.request(request).await?)
72 }
73
74 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#[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 pub fn from_hyper_client_builder(inner: HyperClientBuilder) -> Self {
118 Self(inner)
119 }
120
121 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 pub fn pool_idle_timeout(&mut self, val: Option<Duration>) -> &mut Self {
135 self.0.pool_idle_timeout(val);
136 self
137 }
138
139 pub fn http2_only(&mut self, val: bool) -> &mut Self {
145 self.0.http2_only(val);
146 self
147 }
148
149 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 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 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
222pub struct RequestBuilder<'a> {
233 client: &'a Client,
234 details: RequestDetails,
235}
236
237impl<'a> RequestBuilder<'a> {
238 pub fn body<B: Into<RequestBody>>(mut self, body: B) -> Self {
240 self.details.body = Some(body.into());
241 self
242 }
243
244 pub fn headers(mut self, headers: HeaderMap) -> Self {
246 self.details.headers = headers;
247 self
248 }
249
250 pub fn header<H: Header>(mut self, header: H) -> Self {
254 self.details.headers.typed_insert(header);
255 self
256 }
257
258 pub fn build(self) -> Result<Request<RequestBody>, Error> {
263 self.details.into_request()
264 }
265
266 pub async fn send(self) -> Result<Response, Error> {
271 self.details.send(&self.client).await
272 }
273}
274
275#[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 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 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}