Skip to main content

opentelemetry_http/
lib.rs

1//! HTTP types and client adapters shared by OpenTelemetry components.
2//!
3//! [`HeaderInjector`] and [`HeaderExtractor`] adapt an [`http::HeaderMap`] to
4//! OpenTelemetry's text-map propagation interfaces. [`HttpClient`] is the
5//! transport abstraction used by exporters and other components that issue
6//! HTTP requests.
7//!
8//! # HTTP clients
9//!
10//! This crate does not enable a concrete HTTP client by default. Select one of
11//! these features when an OpenTelemetry component does not provide one:
12//!
13//! - `reqwest` implements [`HttpClient`] for the asynchronous
14//!   `reqwest::Client`.
15//! - `reqwest-blocking` additionally implements [`HttpClient`] for
16//!   `reqwest::blocking::Client`.
17//! - `reqwest-rustls` enables reqwest with its Rustls TLS backend.
18//! - `hyper` provides `hyper::HyperClient`, including support for custom
19//!   connectors.
20//!
21//! The reqwest implementations use the timeout configured on the supplied
22//! reqwest client. `hyper::HyperClient` requires a Tokio runtime and uses
23//! `hyper_util::rt::TokioExecutor` internally.
24//!
25//! # Implementing a client
26//!
27//! A custom client controls connection management, redirects, and timeouts.
28//! `send_bytes` returns HTTP responses regardless of their status code;
29//! transport failures and timeouts are returned as errors. Use
30//! [`ResponseExt::error_for_status`] when non-success status codes should be
31//! converted into errors.
32//!
33//! ```
34//! use async_trait::async_trait;
35//! use opentelemetry_http::{Bytes, HttpClient, HttpError, Request, Response};
36//!
37//! #[derive(Debug)]
38//! struct ExampleClient;
39//!
40//! #[async_trait]
41//! impl HttpClient for ExampleClient {
42//!     async fn send_bytes(
43//!         &self,
44//!         request: Request<Bytes>,
45//!     ) -> Result<Response<Bytes>, HttpError> {
46//!         // A real implementation would send the request and enforce its
47//!         // configured timeout while collecting the complete response body.
48//!         Ok(Response::new(request.into_body()))
49//!     }
50//! }
51//!
52//! let request = Request::post("http://collector.example/v1/traces")
53//!     .body(Bytes::from_static(b"encoded telemetry"))?;
54//! let response = futures_executor::block_on(ExampleClient.send_bytes(request))?;
55//! assert_eq!(response.body(), &Bytes::from_static(b"encoded telemetry"));
56//! # Ok::<(), HttpError>(())
57//! ```
58//!
59//! # Response size limit
60//!
61//! The built-in reqwest and Hyper implementations collect response bodies into
62//! memory and reject bodies larger than 4 MiB with [`ResponseBodyTooLarge`].
63
64use 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
73/// Helper for injecting headers into HTTP Requests. This is used for OpenTelemetry context
74/// propagation over HTTP.
75/// See [this](https://github.com/open-telemetry/opentelemetry-rust/blob/main/examples/tracing-http-propagator/README.md)
76/// for example usage.
77pub struct HeaderInjector<'a>(pub &'a mut http::HeaderMap);
78
79impl Injector for HeaderInjector<'_> {
80    /// Set a key and value in the HeaderMap.  Does nothing if the key or value are not valid inputs.
81    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    /// Reserves capacity for at least `additional` more entries to be inserted.
90    fn reserve(&mut self, additional: usize) {
91        self.0.reserve(additional);
92    }
93}
94
95/// Helper for extracting headers from HTTP Requests. This is used for OpenTelemetry context
96/// propagation over HTTP.
97/// See [this](https://github.com/open-telemetry/opentelemetry-rust/blob/main/examples/tracing-http-propagator/README.md)
98/// for example usage.
99pub struct HeaderExtractor<'a>(pub &'a http::HeaderMap);
100
101impl Extractor for HeaderExtractor<'_> {
102    /// Get a value for a key from the HeaderMap.  If the value is not valid ASCII, returns None.
103    fn get(&self, key: &str) -> Option<&str> {
104        self.0.get(key).and_then(|value| value.to_str().ok())
105    }
106
107    /// Collect all the keys from the HeaderMap.
108    fn keys(&self) -> Vec<&str> {
109        self.0
110            .keys()
111            .map(|value| value.as_str())
112            .collect::<Vec<_>>()
113    }
114
115    /// Get all the values for a key from the HeaderMap
116    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
126/// Error returned when an HTTP request cannot be completed.
127pub type HttpError = Box<dyn std::error::Error + Send + Sync + 'static>;
128
129/// A minimal interface for sending byte-oriented HTTP requests.
130///
131/// This is primarily used for exporting telemetry and for fetching remote
132/// sampling strategies. Implementations are responsible for enforcing any
133/// required request timeout, including while reading the response body.
134///
135/// HTTP clients may depend on a particular async runtime. This trait allows
136/// users to supply an implementation suitable for their runtime.
137#[async_trait]
138pub trait HttpClient: Debug + Send + Sync {
139    /// Send the specified HTTP request with `Bytes` payload.
140    ///
141    /// Returns the complete HTTP response, including non-success status codes.
142    ///
143    /// Returns an error if the request cannot be completed, for example because
144    /// of a connection failure, timeout, redirect failure, or response body
145    /// larger than 4 MiB in a built-in client.
146    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/// Error returned when an HTTP response body exceeds the configured size limit.
153///
154/// Construct this error with [`Self::new`] or [`Default::default`]. Its fields
155/// are private to allow future diagnostic details without changing construction.
156#[derive(Debug, Default)]
157pub struct ResponseBodyTooLarge {
158    _private: (),
159}
160
161impl ResponseBodyTooLarge {
162    /// Creates an error indicating that the response body exceeded the size limit.
163    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    /// An [`HttpClient`] backed by Hyper.
266    ///
267    /// This client requires a Tokio runtime and uses
268    /// [`hyper_util::rt::TokioExecutor`] to drive connections. Responses larger
269    /// than 4 MiB are rejected with [`ResponseBodyTooLarge`].
270    #[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        /// Creates a client with a custom Hyper connector.
285        ///
286        /// The connector must satisfy Hyper's [`Connect`] bounds. `timeout`
287        /// configures the request deadline. When `authorization` is provided,
288        /// its value replaces any `Authorization` header already present on
289        /// each request.
290        pub fn new(connector: C, timeout: Duration, authorization: Option<HeaderValue>) -> Self {
291            // TODO - support custom executor
292            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        /// Creates a client with Hyper's default [`HttpConnector`].
303        ///
304        /// `timeout` configures the request deadline. When `authorization` is
305        /// provided, its value replaces any `Authorization` header already
306        /// present on each request.
307        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
367/// Methods to make working with responses from the [`HttpClient`] trait easier.
368///
369/// This trait is sealed and cannot be implemented outside of this crate.
370pub trait ResponseExt: private::Sealed + Sized {
371    /// Turn a response into an error if the HTTP status does not indicate success (200 - 299).
372    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        // Test that reserve doesn't panic and works correctly
475        {
476            let mut injector = HeaderInjector(&mut carrier);
477            injector.reserve(10);
478
479            // Verify the HeaderMap still works after reserve
480            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        // Test reserve with zero capacity
488        {
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        // Test that capacity is actually reserved (at least the requested amount)
499        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        // Add some headers and verify capacity doesn't decrease
507        {
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}