1use async_trait::async_trait;
65use std::fmt::Debug;
66
67#[doc(no_inline)]
68pub use bytes::Bytes;
69#[doc(no_inline)]
70pub use http::{Request, Response};
71use opentelemetry::propagation::{Extractor, Injector};
72
73pub struct HeaderInjector<'a>(pub &'a mut http::HeaderMap);
78
79impl Injector for HeaderInjector<'_> {
80 fn set(&mut self, key: &str, value: String) {
82 if let Ok(name) = http::header::HeaderName::from_bytes(key.as_bytes()) {
83 if let Ok(val) = http::header::HeaderValue::from_str(&value) {
84 self.0.insert(name, val);
85 }
86 }
87 }
88
89 fn reserve(&mut self, additional: usize) {
91 self.0.reserve(additional);
92 }
93}
94
95pub struct HeaderExtractor<'a>(pub &'a http::HeaderMap);
100
101impl Extractor for HeaderExtractor<'_> {
102 fn get(&self, key: &str) -> Option<&str> {
104 self.0.get(key).and_then(|value| value.to_str().ok())
105 }
106
107 fn keys(&self) -> Vec<&str> {
109 self.0
110 .keys()
111 .map(|value| value.as_str())
112 .collect::<Vec<_>>()
113 }
114
115 fn get_all(&self, key: &str) -> Option<Vec<&str>> {
117 let all_iter = self.0.get_all(key).iter();
118 if let (0, Some(0)) = all_iter.size_hint() {
119 return None;
120 }
121
122 Some(all_iter.filter_map(|value| value.to_str().ok()).collect())
123 }
124}
125
126pub type HttpError = Box<dyn std::error::Error + Send + Sync + 'static>;
128
129#[async_trait]
138pub trait HttpClient: Debug + Send + Sync {
139 async fn send_bytes(&self, request: Request<Bytes>) -> Result<Response<Bytes>, HttpError>;
147}
148
149#[cfg(any(feature = "reqwest", feature = "hyper"))]
150const MAX_RESPONSE_BODY_BYTES: usize = 4 * 1024 * 1024;
151
152#[derive(Debug, Default)]
157pub struct ResponseBodyTooLarge {
158 _private: (),
159}
160
161impl ResponseBodyTooLarge {
162 pub fn new() -> Self {
164 Self::default()
165 }
166}
167
168impl std::fmt::Display for ResponseBodyTooLarge {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 write!(f, "response body exceeded maximum allowed 4 MiB limit")
171 }
172}
173
174impl std::error::Error for ResponseBodyTooLarge {}
175
176#[cfg(feature = "reqwest")]
177mod reqwest {
178 use opentelemetry::otel_debug;
179
180 use crate::ResponseBodyTooLarge;
181
182 use super::{
183 async_trait, Bytes, HttpClient, HttpError, Request, Response, MAX_RESPONSE_BODY_BYTES,
184 };
185
186 #[async_trait]
187 impl HttpClient for reqwest::Client {
188 async fn send_bytes(&self, request: Request<Bytes>) -> Result<Response<Bytes>, HttpError> {
189 otel_debug!(name: "ReqwestClient.Send");
190 let request = request.try_into()?;
191 let mut response = self.execute(request).await?;
192 let capacity = response
193 .content_length()
194 .unwrap_or(0)
195 .min(MAX_RESPONSE_BODY_BYTES as u64) as usize;
196
197 let mut body_bytes = bytes::BytesMut::with_capacity(capacity);
198
199 let status = response.status();
200 let headers = std::mem::take(response.headers_mut());
201 while let Some(chunk) = response.chunk().await? {
202 if body_bytes.len() + chunk.len() > MAX_RESPONSE_BODY_BYTES {
203 return Err(Box::new(ResponseBodyTooLarge::new()));
204 }
205 body_bytes.extend_from_slice(&chunk);
206 }
207 let mut http_response = Response::builder()
208 .status(status)
209 .body(body_bytes.freeze())?;
210
211 *http_response.headers_mut() = headers;
212 Ok(http_response)
213 }
214 }
215
216 #[cfg(not(target_arch = "wasm32"))]
217 #[cfg(feature = "reqwest-blocking")]
218 #[async_trait]
219 impl HttpClient for reqwest::blocking::Client {
220 async fn send_bytes(&self, request: Request<Bytes>) -> Result<Response<Bytes>, HttpError> {
221 use std::io::Read;
222 otel_debug!(name: "ReqwestBlockingClient.Send");
223 let request = request.try_into()?;
224 let mut response = self.execute(request)?;
225 let capacity = response
226 .content_length()
227 .unwrap_or(0)
228 .min(MAX_RESPONSE_BODY_BYTES as u64) as usize;
229 let status = response.status();
230 let headers = std::mem::take(response.headers_mut());
231 let mut body_bytes = Vec::with_capacity(capacity);
232 response
233 .take(MAX_RESPONSE_BODY_BYTES as u64 + 1)
234 .read_to_end(&mut body_bytes)?;
235 if body_bytes.len() > MAX_RESPONSE_BODY_BYTES {
236 return Err(Box::new(ResponseBodyTooLarge::new()));
237 }
238 let mut http_response = Response::builder()
239 .status(status)
240 .body(Bytes::from(body_bytes))?;
241 *http_response.headers_mut() = headers;
242 Ok(http_response)
243 }
244 }
245}
246
247#[cfg(feature = "hyper")]
248pub mod hyper {
249 use super::{
250 async_trait, Bytes, HttpClient, HttpError, Request, Response, MAX_RESPONSE_BODY_BYTES,
251 };
252 use crate::ResponseBodyTooLarge;
253 use http::HeaderValue;
254 use http_body_util::{BodyExt, Full};
255 use hyper::body::Body as _;
256 use hyper_util::client::legacy::{
257 connect::{Connect, HttpConnector},
258 Client,
259 };
260 use opentelemetry::otel_debug;
261 use std::fmt::Debug;
262 use std::time::Duration;
263 use tokio::time;
264
265 #[derive(Debug, Clone)]
271 pub struct HyperClient<C = HttpConnector>
272 where
273 C: Connect + Clone + Send + Sync + 'static,
274 {
275 inner: Client<C, Full<Bytes>>,
276 timeout: Duration,
277 authorization: Option<HeaderValue>,
278 }
279
280 impl<C> HyperClient<C>
281 where
282 C: Connect + Clone + Send + Sync + 'static,
283 {
284 pub fn new(connector: C, timeout: Duration, authorization: Option<HeaderValue>) -> Self {
291 let inner = Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector);
293 Self {
294 inner,
295 timeout,
296 authorization,
297 }
298 }
299 }
300
301 impl HyperClient<HttpConnector> {
302 pub fn with_default_connector(
308 timeout: Duration,
309 authorization: Option<HeaderValue>,
310 ) -> Self {
311 Self::new(HttpConnector::new(), timeout, authorization)
312 }
313 }
314
315 #[async_trait]
316 impl<C> HttpClient for HyperClient<C>
317 where
318 C: Connect + Clone + Send + Sync + 'static,
319 HyperClient<C>: Debug,
320 {
321 async fn send_bytes(&self, request: Request<Bytes>) -> Result<Response<Bytes>, HttpError> {
322 otel_debug!(name: "HyperClient.Send");
323 let (parts, body) = request.into_parts();
324 let mut request = Request::from_parts(parts, Full::from(body));
325 if let Some(ref authorization) = self.authorization {
326 request
327 .headers_mut()
328 .insert(http::header::AUTHORIZATION, authorization.clone());
329 }
330 time::timeout(self.timeout, async {
331 let mut response = self.inner.request(request).await?;
332 let capacity = response
333 .body()
334 .size_hint()
335 .upper()
336 .unwrap_or(0)
337 .min(MAX_RESPONSE_BODY_BYTES as u64) as usize;
338 let mut body_bytes = bytes::BytesMut::with_capacity(capacity);
339 let status = response.status();
340 let headers = std::mem::take(response.headers_mut());
341 let mut body = response.into_body();
342 while let Some(frame) = body.frame().await {
343 let frame = frame?;
344 if let Ok(chunk) = frame.into_data() {
345 if body_bytes.len() + chunk.len() > MAX_RESPONSE_BODY_BYTES {
346 return Err(Box::new(ResponseBodyTooLarge::new()) as HttpError);
347 }
348 body_bytes.extend_from_slice(&chunk);
349 }
350 }
351 let mut http_response = Response::builder()
352 .status(status)
353 .body(body_bytes.freeze())?;
354 *http_response.headers_mut() = headers;
355 Ok(http_response)
356 })
357 .await?
358 }
359 }
360}
361
362mod private {
363 pub trait Sealed {}
364 impl<T> Sealed for http::Response<T> {}
365}
366
367pub trait ResponseExt: private::Sealed + Sized {
371 fn error_for_status(self) -> Result<Self, HttpError>;
373}
374
375impl<T> ResponseExt for Response<T> {
376 fn error_for_status(self) -> Result<Self, HttpError> {
377 if self.status().is_success() {
378 Ok(self)
379 } else {
380 Err(format!("request failed with status {}", self.status()).into())
381 }
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use http::HeaderValue;
389
390 #[test]
391 fn response_body_too_large_construction() {
392 for error in [ResponseBodyTooLarge::new(), ResponseBodyTooLarge::default()] {
393 let error: HttpError = Box::new(error);
394 assert!(error.downcast_ref::<ResponseBodyTooLarge>().is_some());
395 assert_eq!(
396 error.to_string(),
397 "response body exceeded maximum allowed 4 MiB limit"
398 );
399 }
400 }
401
402 #[cfg(all(
403 any(feature = "hyper", feature = "reqwest", feature = "reqwest-blocking"),
404 not(target_arch = "wasm32")
405 ))]
406 use std::io::{Read, Write};
407
408 #[cfg(all(
409 any(feature = "hyper", feature = "reqwest", feature = "reqwest-blocking"),
410 not(target_arch = "wasm32")
411 ))]
412 use std::net::{SocketAddr, TcpListener};
413
414 #[cfg(all(
415 any(feature = "hyper", feature = "reqwest", feature = "reqwest-blocking"),
416 not(target_arch = "wasm32")
417 ))]
418 use std::thread::JoinHandle;
419
420 #[test]
421 fn http_headers_get() {
422 let mut carrier = http::HeaderMap::new();
423 HeaderInjector(&mut carrier).set("headerName", "value".to_string());
424
425 assert_eq!(
426 HeaderExtractor(&carrier).get("HEADERNAME"),
427 Some("value"),
428 "case insensitive extraction"
429 )
430 }
431 #[test]
432 fn http_headers_get_all() {
433 let mut carrier = http::HeaderMap::new();
434 carrier.append("headerName", HeaderValue::from_static("value"));
435 carrier.append("headerName", HeaderValue::from_static("value2"));
436 carrier.append("headerName", HeaderValue::from_static("value3"));
437
438 assert_eq!(
439 HeaderExtractor(&carrier).get_all("HEADERNAME"),
440 Some(vec!["value", "value2", "value3"]),
441 "all values from a key extraction"
442 )
443 }
444
445 #[test]
446 fn http_headers_get_all_missing_key() {
447 let mut carrier = http::HeaderMap::new();
448 carrier.append("headerName", HeaderValue::from_static("value"));
449
450 assert_eq!(
451 HeaderExtractor(&carrier).get_all("not_existing"),
452 None,
453 "all values from a missing key extraction"
454 )
455 }
456
457 #[test]
458 fn http_headers_keys() {
459 let mut carrier = http::HeaderMap::new();
460 HeaderInjector(&mut carrier).set("headerName1", "value1".to_string());
461 HeaderInjector(&mut carrier).set("headerName2", "value2".to_string());
462
463 let extractor = HeaderExtractor(&carrier);
464 let got = extractor.keys();
465 assert_eq!(got.len(), 2);
466 assert!(got.contains(&"headername1"));
467 assert!(got.contains(&"headername2"));
468 }
469
470 #[test]
471 fn http_headers_reserve() {
472 let mut carrier = http::HeaderMap::new();
473
474 {
476 let mut injector = HeaderInjector(&mut carrier);
477 injector.reserve(10);
478
479 injector.set("test-header", "test-value".to_string());
481 }
482 assert_eq!(
483 HeaderExtractor(&carrier).get("test-header"),
484 Some("test-value")
485 );
486
487 {
489 let mut injector = HeaderInjector(&mut carrier);
490 injector.reserve(0);
491 injector.set("another-header", "another-value".to_string());
492 }
493 assert_eq!(
494 HeaderExtractor(&carrier).get("another-header"),
495 Some("another-value")
496 );
497
498 let mut new_carrier = http::HeaderMap::new();
500 {
501 let mut new_injector = HeaderInjector(&mut new_carrier);
502 new_injector.reserve(5);
503 }
504 let initial_capacity = new_carrier.capacity();
505
506 {
508 let mut new_injector = HeaderInjector(&mut new_carrier);
509 for i in 0..3 {
510 new_injector.set(&format!("header-{}", i), format!("value-{}", i));
511 }
512 }
513
514 assert!(new_carrier.capacity() >= initial_capacity);
515 assert!(new_carrier.capacity() >= 5);
516 }
517
518 #[test]
519 fn error_for_status_matches_http_status_class() {
520 for status in [http::StatusCode::OK, http::StatusCode::NO_CONTENT] {
521 let response = Response::builder().status(status).body(()).unwrap();
522 assert!(response.error_for_status().is_ok());
523 }
524
525 for status in [
526 http::StatusCode::MOVED_PERMANENTLY,
527 http::StatusCode::BAD_REQUEST,
528 http::StatusCode::TOO_MANY_REQUESTS,
529 http::StatusCode::INTERNAL_SERVER_ERROR,
530 ] {
531 let response = Response::builder().status(status).body(()).unwrap();
532 assert!(response.error_for_status().is_err());
533 }
534 }
535
536 #[cfg(all(
537 any(feature = "hyper", feature = "reqwest", feature = "reqwest-blocking"),
538 not(target_arch = "wasm32")
539 ))]
540 fn spawn_error_response_server() -> (SocketAddr, JoinHandle<()>) {
541 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
542 let address = listener.local_addr().unwrap();
543 let server = std::thread::spawn(move || {
544 let (mut stream, _) = listener.accept().unwrap();
545 let mut request = [0; 1024];
546 let _ = stream.read(&mut request).unwrap();
547 stream
548 .write_all(
549 b"HTTP/1.1 429 Too Many Requests\r\n\
550Retry-After: 7\r\n\
551Content-Length: 0\r\n\
552Connection: close\r\n\r\n",
553 )
554 .unwrap();
555 });
556 (address, server)
557 }
558
559 #[cfg(all(feature = "reqwest-blocking", not(target_arch = "wasm32")))]
560 #[test]
561 fn reqwest_blocking_preserves_error_response_status_and_headers() {
562 let (address, server) = spawn_error_response_server();
563 let client = ::reqwest::blocking::Client::new();
564 let request = Request::post(format!("http://{address}/v1/traces"))
565 .body(Bytes::new())
566 .unwrap();
567 let response = futures_executor::block_on(client.send_bytes(request)).unwrap();
568
569 server.join().unwrap();
570 assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
571 assert_eq!(response.headers().get("retry-after").unwrap(), "7");
572 }
573
574 #[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
575 #[test]
576 fn reqwest_async_preserves_error_response_status_and_headers() {
577 let (address, server) = spawn_error_response_server();
578 let client = ::reqwest::Client::new();
579 let request = Request::post(format!("http://{address}/v1/traces"))
580 .body(Bytes::new())
581 .unwrap();
582 let runtime = tokio::runtime::Builder::new_current_thread()
583 .enable_all()
584 .build()
585 .unwrap();
586 let response = runtime.block_on(client.send_bytes(request)).unwrap();
587
588 server.join().unwrap();
589 assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
590 assert_eq!(response.headers().get("retry-after").unwrap(), "7");
591 }
592
593 #[cfg(all(feature = "hyper", not(target_arch = "wasm32")))]
594 #[test]
595 fn hyper_preserves_error_response_status_and_headers() {
596 let (address, server) = spawn_error_response_server();
597 let client = crate::hyper::HyperClient::with_default_connector(
598 std::time::Duration::from_secs(2),
599 None,
600 );
601 let request = Request::post(format!("http://{address}/v1/traces"))
602 .body(Bytes::new())
603 .unwrap();
604 let runtime = tokio::runtime::Builder::new_current_thread()
605 .enable_all()
606 .build()
607 .unwrap();
608 let response = runtime.block_on(client.send_bytes(request)).unwrap();
609
610 server.join().unwrap();
611 assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
612 assert_eq!(response.headers().get("retry-after").unwrap(), "7");
613 }
614
615 #[cfg(all(
616 test,
617 any(feature = "reqwest", feature = "reqwest-blocking", feature = "hyper")
618 ))]
619 mod body_limit_tests {
620 use super::MAX_RESPONSE_BODY_BYTES;
621 use crate::HttpClient;
622 use bytes::Bytes;
623 use http::Request;
624 #[cfg(feature = "hyper")]
625 use std::future::Future;
626 use std::net::SocketAddr;
627 #[cfg(feature = "hyper")]
628 use std::pin::Pin;
629 #[cfg(feature = "hyper")]
630 use std::task::{Context, Poll};
631 use tokio::io::{AsyncReadExt, AsyncWriteExt};
632 use tokio::net::TcpListener;
633
634 #[cfg(feature = "hyper")]
635 #[derive(Clone, Debug)]
636 struct LocalConnector(SocketAddr);
637
638 #[cfg(feature = "hyper")]
639 impl tower_service::Service<http::Uri> for LocalConnector {
640 type Response = hyper_util::rt::TokioIo<tokio::net::TcpStream>;
641 type Error = std::io::Error;
642 type Future =
643 Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
644
645 fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
646 Poll::Ready(Ok(()))
647 }
648
649 fn call(&mut self, _uri: http::Uri) -> Self::Future {
650 let address = self.0;
651 Box::pin(async move {
652 tokio::net::TcpStream::connect(address)
653 .await
654 .map(hyper_util::rt::TokioIo::new)
655 })
656 }
657 }
658
659 async fn start_server(body_size: usize) -> SocketAddr {
660 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
661 let addr = listener.local_addr().unwrap();
662 tokio::spawn(async move {
663 if let Ok((mut socket, _)) = listener.accept().await {
664 let mut buf = [0u8; 1024];
665 let _ = socket.read(&mut buf).await;
666 let body = vec![b'a'; body_size];
667 let response = format!(
668 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
669 body.len()
670 );
671 let _ = socket.write_all(response.as_bytes()).await;
672 let _ = socket.write_all(&body).await;
673 let _ = socket.shutdown().await;
674 }
675 });
676 addr
677 }
678
679 #[cfg(feature = "hyper")]
680 async fn start_stalled_body_server() -> SocketAddr {
681 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
682 let addr = listener.local_addr().unwrap();
683 tokio::spawn(async move {
684 if let Ok((mut socket, _)) = listener.accept().await {
685 let mut buf = [0u8; 1024];
686 let _ = socket.read(&mut buf).await;
687 let _ = socket
688 .write_all(
689 b"HTTP/1.1 200 OK\r\nContent-Length: 1\r\nConnection: close\r\n\r\n",
690 )
691 .await;
692 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
693 }
694 });
695 addr
696 }
697
698 async fn assert_body_size(client: &dyn HttpClient, addr: SocketAddr, expected_size: usize) {
699 let request = Request::builder()
700 .method("POST")
701 .uri(format!("http://{}/", addr))
702 .body(Bytes::new())
703 .unwrap();
704 let response = client.send_bytes(request).await.unwrap();
705 assert_eq!(response.body().len(), expected_size);
706 }
707
708 async fn assert_exceeds_limit(client: &dyn HttpClient, addr: SocketAddr) {
709 let request = Request::builder()
710 .method("POST")
711 .uri(format!("http://{}/", addr))
712 .body(Bytes::new())
713 .unwrap();
714 let error = client.send_bytes(request).await.unwrap_err();
715 assert!(error
716 .downcast_ref::<crate::ResponseBodyTooLarge>()
717 .is_some());
718 }
719
720 #[cfg(feature = "reqwest-blocking")]
721 fn start_blocking_server(body_size: usize) -> SocketAddr {
722 use std::io::{Read, Write};
723 use std::net::TcpListener;
724
725 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
726 let addr = listener.local_addr().unwrap();
727
728 std::thread::spawn(move || {
729 if let Ok((mut socket, _)) = listener.accept() {
730 let mut buf = [0u8; 1024];
731 let _ = socket.read(&mut buf);
732
733 let body = vec![b'a'; body_size];
734 let response = format!(
735 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
736 body.len()
737 );
738
739 let _ = socket.write_all(response.as_bytes());
740 let _ = socket.write_all(&body);
741 }
742 });
743
744 addr
745 }
746
747 #[cfg(feature = "reqwest")]
748 #[tokio::test]
749 async fn reqwest_body_within_limit() {
750 let addr = start_server(MAX_RESPONSE_BODY_BYTES).await;
751 assert_body_size(&reqwest::Client::new(), addr, MAX_RESPONSE_BODY_BYTES).await;
752 }
753
754 #[cfg(feature = "reqwest")]
755 #[tokio::test]
756 async fn reqwest_body_exceeds_limit() {
757 let addr = start_server(MAX_RESPONSE_BODY_BYTES + 1).await;
758 assert_exceeds_limit(&reqwest::Client::new(), addr).await;
759 }
760
761 #[cfg(feature = "reqwest-blocking")]
762 #[test]
763 fn reqwest_blocking_body_within_limit() {
764 let addr = start_blocking_server(MAX_RESPONSE_BODY_BYTES);
765
766 futures_executor::block_on(assert_body_size(
767 &reqwest::blocking::Client::new(),
768 addr,
769 MAX_RESPONSE_BODY_BYTES,
770 ));
771 }
772
773 #[cfg(feature = "reqwest-blocking")]
774 #[test]
775 fn reqwest_blocking_body_exceeds_limit() {
776 let addr = start_blocking_server(MAX_RESPONSE_BODY_BYTES + 1);
777
778 futures_executor::block_on(assert_exceeds_limit(
779 &reqwest::blocking::Client::new(),
780 addr,
781 ));
782 }
783
784 #[cfg(feature = "hyper")]
785 #[tokio::test]
786 async fn hyper_body_within_limit() {
787 let addr = start_server(MAX_RESPONSE_BODY_BYTES).await;
788 let client = crate::hyper::HyperClient::with_default_connector(
789 std::time::Duration::from_secs(5),
790 None,
791 );
792 assert_body_size(&client, addr, MAX_RESPONSE_BODY_BYTES).await;
793 }
794
795 #[cfg(feature = "hyper")]
796 #[tokio::test]
797 async fn hyper_client_new_accepts_custom_connector() {
798 let addr = start_server(100).await;
799 let client = crate::hyper::HyperClient::new(
800 LocalConnector(addr),
801 std::time::Duration::from_secs(5),
802 None,
803 );
804 assert_body_size(&client, addr, 100).await;
805 }
806 #[cfg(feature = "hyper")]
807 #[tokio::test]
808 async fn hyper_body_exceeds_limit() {
809 let addr = start_server(MAX_RESPONSE_BODY_BYTES + 1).await;
810 let client = crate::hyper::HyperClient::with_default_connector(
811 std::time::Duration::from_secs(5),
812 None,
813 );
814 assert_exceeds_limit(&client, addr).await;
815 }
816
817 #[cfg(feature = "hyper")]
818 #[tokio::test]
819 async fn hyper_timeout_covers_response_body() {
820 let addr = start_stalled_body_server().await;
821 let client = crate::hyper::HyperClient::with_default_connector(
822 std::time::Duration::from_millis(25),
823 None,
824 );
825 let request = Request::post(format!("http://{addr}/"))
826 .body(Bytes::new())
827 .unwrap();
828
829 let error = tokio::time::timeout(
830 std::time::Duration::from_millis(200),
831 client.send_bytes(request),
832 )
833 .await
834 .expect("HyperClient must enforce its configured timeout")
835 .expect_err("stalled response body must time out");
836
837 assert!(error
838 .downcast_ref::<tokio::time::error::Elapsed>()
839 .is_some());
840 }
841 }
842}