Skip to main content

volo_http/client/transport/
protocol.rs

1//! Protocol related implementations
2
3use std::{error::Error, str::FromStr, sync::LazyLock};
4
5use futures::{
6    FutureExt, TryFutureExt,
7    future::{self, Either},
8};
9use http::{
10    header,
11    uri::{Authority, Scheme, Uri},
12    version::Version,
13};
14use hyper::client::conn;
15use hyper_util::rt::TokioIo;
16use motore::{make::MakeConnection, service::Service};
17use volo::{context::Context, net::Address};
18
19use super::{
20    connector::{HttpMakeConnection, PeerInfo},
21    pool::{self, Connecting, Pool, Poolable, Pooled, Reservation},
22};
23use crate::{
24    body::Body,
25    context::ClientContext,
26    error::{
27        BoxError, ClientError,
28        client::{Result, connect_error, no_address, request_error, retry, tri},
29    },
30    request::Request,
31    response::Response,
32    utils::lazy::Started,
33};
34
35/// Configuration of HTTP/1
36#[derive(Default)]
37pub(crate) struct ClientConfig {
38    #[cfg(feature = "http1")]
39    pub h1: super::http1::Config,
40    #[cfg(feature = "http2")]
41    pub h2: super::http2::Config,
42}
43
44#[derive(Clone)]
45pub(crate) struct ClientTransportConfig {
46    pub stat_enable: bool,
47    #[cfg(feature = "__tls")]
48    #[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "native-tls"))))]
49    pub disable_tls: bool,
50}
51
52impl Default for ClientTransportConfig {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl ClientTransportConfig {
59    pub fn new() -> Self {
60        Self {
61            stat_enable: true,
62            #[cfg(feature = "__tls")]
63            disable_tls: false,
64        }
65    }
66}
67
68/// Transport service of HTTP Client.
69///
70/// This service will connect to the [`Address`] of callee's [`Endpoint`] in [`ClientContext`], then
71/// send a [`Request`] to the destination server, and return a [`Response`] the server response.
72///
73/// [`Endpoint`]: volo::context::Endpoint
74/// [`Request`]: http::request::Request
75/// [`Response`]: http::response::Response
76pub struct ClientTransport<B = Body> {
77    #[cfg(feature = "http1")]
78    h1_client: conn::http1::Builder,
79    #[cfg(feature = "http2")]
80    h2_client: conn::http2::Builder<hyper_util::rt::TokioExecutor>,
81    config: ClientTransportConfig,
82    connector: HttpMakeConnection,
83    pool: Pool<PoolKey, HttpConnection<B>>,
84}
85
86#[cfg(feature = "__tls")]
87type PoolKey = (Scheme, Address, Option<faststr::FastStr>);
88
89#[cfg(not(feature = "__tls"))]
90type PoolKey = (Scheme, Address);
91
92impl<B> ClientTransport<B> {
93    pub(crate) fn new(
94        http_config: ClientConfig,
95        transport_config: ClientTransportConfig,
96        pool_config: pool::Config,
97        #[cfg(feature = "__tls")] tls_connector: Option<volo::net::tls::TlsConnector>,
98    ) -> Self {
99        #[cfg(feature = "http1")]
100        let h1_client = super::http1::client(&http_config.h1);
101        #[cfg(feature = "http2")]
102        let h2_client = super::http2::client(&http_config.h2);
103
104        let builder = HttpMakeConnection::builder(&transport_config);
105        #[cfg(feature = "__tls")]
106        let builder = match tls_connector {
107            Some(connector) => builder.with_tls_connector(connector),
108            None => builder,
109        };
110        let connector = builder.build();
111
112        Self {
113            #[cfg(feature = "http1")]
114            h1_client,
115            #[cfg(feature = "http2")]
116            h2_client,
117            config: transport_config,
118            connector,
119            pool: Pool::new(pool_config),
120        }
121    }
122
123    fn connect_to(
124        &self,
125        ver: pool::Ver,
126        peer: PeerInfo,
127    ) -> impl Started<Output = Result<Pooled<PoolKey, HttpConnection<B>>>> + Send + 'static
128    where
129        B: http_body::Body + Unpin + Send + 'static,
130        B::Data: Send,
131        B::Error: Into<BoxError> + 'static,
132    {
133        let key = pool_key(&peer);
134        let connector = self.connector.clone();
135        let pool = self.pool.clone();
136        #[cfg(feature = "http1")]
137        let h1_client = self.h1_client.clone();
138        #[cfg(feature = "http2")]
139        let h2_client = self.h2_client.clone();
140
141        crate::utils::lazy::lazy(move || {
142            let connecting = match pool.connecting(&key, ver) {
143                Some(lock) => lock,
144                None => return Either::Right(future::err(retry())),
145            };
146            Either::Left(Box::pin(connect_impl(
147                ver,
148                peer,
149                connector,
150                pool,
151                connecting,
152                #[cfg(feature = "http1")]
153                h1_client,
154                #[cfg(feature = "http2")]
155                h2_client,
156            )))
157        })
158    }
159
160    async fn pooled_connect(
161        &self,
162        ver: Version,
163        peer: PeerInfo,
164    ) -> Result<Pooled<PoolKey, HttpConnection<B>>>
165    where
166        B: http_body::Body + Unpin + Send + 'static,
167        B::Data: Send,
168        B::Error: Into<BoxError> + 'static,
169    {
170        let key = pool_key(&peer);
171
172        let checkout = self.pool.checkout(key);
173        let connect = self.connect_to(ver.into(), peer);
174
175        // Well, `futures::future::select` is more suitable than `tokio::select!` in this case.
176        match future::select(checkout, connect).await {
177            Either::Left((Ok(checked_out), connecting)) => {
178                // Checkout is done while connecting is started
179                if connecting.started() {
180                    let conn_fut = connecting
181                        .map_err(|err| tracing::trace!("background connect error: {err}"))
182                        .map(|_pooled| {
183                            // Drop the `Pooled` and put it into pool in `Drop`
184                        });
185                    // Spawn it for finishing the connecting
186                    tokio::spawn(conn_fut);
187                }
188                Ok(checked_out)
189            }
190            Either::Right((Ok(connected), _checkout)) => Ok(connected),
191            Either::Left((Err(err), connecting)) => {
192                // The checked out connection was closed, just continue the connecting
193                if err.is_canceled() {
194                    connecting.await
195                } else {
196                    // unreachable?
197                    Err(connect_error(err))
198                }
199            }
200            Either::Right((Err(err), checkout)) => {
201                // The connection failed while acquiring the pool lock, and we should retry the
202                // checkout.
203                if err
204                    .source()
205                    .is_some_and(<dyn Error>::is::<crate::error::client::Retry>)
206                {
207                    checkout.await.map_err(connect_error)
208                } else {
209                    // Unexpected connect error
210                    Err(err)
211                }
212            }
213        }
214    }
215}
216
217fn pool_key(peer: &PeerInfo) -> PoolKey {
218    (
219        peer.scheme.clone(),
220        peer.address.clone(),
221        #[cfg(feature = "__tls")]
222        (peer.scheme == Scheme::HTTPS).then(|| peer.name.clone()),
223    )
224}
225
226async fn connect_impl<B>(
227    _ver: pool::Ver,
228    peer: PeerInfo,
229    connector: HttpMakeConnection,
230    pool: Pool<PoolKey, HttpConnection<B>>,
231    connecting: Connecting<PoolKey, HttpConnection<B>>,
232    #[cfg(feature = "http1")] h1_client: conn::http1::Builder,
233    #[cfg(feature = "http2")] h2_client: conn::http2::Builder<hyper_util::rt::TokioExecutor>,
234) -> Result<Pooled<PoolKey, HttpConnection<B>>>
235where
236    B: http_body::Body + Unpin + Send + 'static,
237    B::Data: Send,
238    B::Error: Into<BoxError> + 'static,
239{
240    #[cfg(feature = "http1")]
241    let key = pool_key(&peer);
242
243    let conn = match connector.make_connection(peer).await {
244        Ok(conn) => conn,
245        Err(err) => {
246            tracing::warn!("[Volo-HTTP] failed to make connection: {err}");
247            return Err(err);
248        }
249    };
250
251    #[cfg(feature = "http2")]
252    let use_h2 = conn_use_h2(_ver, &conn);
253    #[cfg(not(feature = "http2"))]
254    let use_h2 = false;
255
256    let conn = TokioIo::new(conn);
257    if use_h2 {
258        #[cfg(feature = "http2")]
259        {
260            let connecting = if _ver == pool::Ver::Auto {
261                tri!(connecting.alpn_h2(&pool).ok_or_else(retry))
262            } else {
263                connecting
264            };
265            let (mut sender, conn) = tri!(h2_client.handshake(conn).await.map_err(connect_error));
266            tokio::spawn(conn);
267            // Wait for `conn` to ready up before we declare self sender as usable.
268            tri!(sender.ready().await.map_err(connect_error));
269            Ok(pool.pooled(connecting, HttpConnection::H2(sender)))
270        }
271        #[cfg(not(feature = "http2"))]
272        Err(crate::error::client::bad_version())
273    } else {
274        #[cfg(feature = "http1")]
275        {
276            let (mut sender, conn) = tri!(h1_client.handshake(conn).await.map_err(connect_error));
277
278            // This channel only returns the sender from the request future to the
279            // connection task.
280            let (return_tx, return_rx) = tokio::sync::mpsc::unbounded_channel();
281
282            // Replace `tokio::spawn(connection)` with a managed wrapper.
283            let driver = ManagedH1Connection {
284                connection: conn,
285                return_rx,
286                waiting: None,
287                returner: pool.return_handle(),
288                key,
289            };
290
291            tokio::spawn(driver);
292
293            // The connect future still owns return_tx, so return_rx cannot close
294            // before the initial readiness check completes.
295            tri!(sender.ready().await.map_err(connect_error));
296
297            let lease = H1Lease::new(sender, return_tx);
298            Ok(pool.pooled(connecting, HttpConnection::H1(lease)))
299        }
300        #[cfg(not(feature = "http1"))]
301        Err(crate::error::client::bad_version())
302    }
303}
304
305#[cfg(feature = "http2")]
306fn conn_use_h2(ver: pool::Ver, _conn: &volo::net::conn::Conn) -> bool {
307    #[cfg(feature = "__tls")]
308    let use_h2 = match _conn.stream.negotiated_alpn().as_deref() {
309        Some(alpn) => {
310            // ALPN negotiated to use H2
311            if alpn == b"h2" {
312                return true;
313            }
314            // ALPN negotiated not to use H2
315            false
316        }
317        // Use H2 by default
318        None => true,
319    };
320    #[cfg(not(feature = "__tls"))]
321    let use_h2 = true;
322
323    // H2 is specified or H1 is disabled
324    if use_h2 && (ver == pool::Ver::Http2 || cfg!(not(feature = "http1"))) {
325        return true;
326    }
327
328    false
329}
330
331impl<B> Service<ClientContext, Request<B>> for ClientTransport<B>
332where
333    B: http_body::Body + Unpin + Send + 'static,
334    B::Data: Send,
335    B::Error: Into<Box<dyn Error + Send + Sync>> + 'static,
336{
337    type Response = Response;
338    type Error = ClientError;
339
340    async fn call(
341        &self,
342        cx: &mut ClientContext,
343        mut req: Request<B>,
344    ) -> Result<Self::Response, Self::Error> {
345        rewrite_uri(cx, &mut req);
346
347        let callee = cx.rpc_info().callee();
348        let address = callee.address().ok_or_else(no_address)?;
349
350        let ver = req.version();
351        let peer = PeerInfo {
352            scheme: cx.target().scheme().cloned().unwrap_or(Scheme::HTTP),
353            address,
354            #[cfg(feature = "__tls")]
355            name: callee.service_name(),
356        };
357
358        let stat_enabled = self.config.stat_enable;
359        if stat_enabled {
360            cx.stats.record_transport_start_at();
361        }
362
363        let mut conn = tri!(self.pooled_connect(ver, peer).await);
364        let res = conn.send_request(req).await;
365
366        if stat_enabled {
367            cx.stats.record_transport_end_at();
368        }
369
370        res
371    }
372}
373
374#[cfg(feature = "http1")]
375struct H1Returned<B> {
376    http_sender: conn::http1::SendRequest<B>,
377    return_tx: tokio::sync::mpsc::UnboundedSender<H1Returned<B>>,
378}
379
380#[cfg(feature = "http1")]
381struct H1Lease<B> {
382    returned: Option<H1Returned<B>>,
383}
384
385#[cfg(feature = "http1")]
386struct H1ReturnGuard<B> {
387    returned: Option<H1Returned<B>>,
388}
389
390#[cfg(feature = "http1")]
391impl<B> H1ReturnGuard<B> {
392    fn http_sender_mut(&mut self) -> &mut conn::http1::SendRequest<B> {
393        &mut self
394            .returned
395            .as_mut()
396            .expect("HTTP/1 sender already returned")
397            .http_sender
398    }
399
400    fn return_to_driver(&mut self) {
401        let Some(returned) = self.returned.take() else {
402            return;
403        };
404
405        // The original tx must keep moving with the message. Use a temporary
406        // clone to perform this send.
407        let tx = returned.return_tx.clone();
408        if let Err(_err) = tx.send(returned) {
409            // The receiver is gone, so the driver for this physical connection
410            // has already stopped. There is no receiver to retry, and a sender
411            // whose readiness is unknown must not be returned directly to the
412            // pool. Dropping SendError also drops the sender and return_tx,
413            // explicitly giving up reuse of this connection.
414            tracing::trace!("HTTP/1 connection driver already closed");
415        };
416    }
417}
418
419#[cfg(feature = "http1")]
420impl<B> Drop for H1ReturnGuard<B> {
421    fn drop(&mut self) {
422        // Cancellation of the send_request future uses the same return path.
423        self.return_to_driver();
424    }
425}
426
427#[cfg(feature = "http1")]
428impl<B> H1Lease<B> {
429    fn new(
430        http_sender: conn::http1::SendRequest<B>,
431        return_tx: tokio::sync::mpsc::UnboundedSender<H1Returned<B>>,
432    ) -> Self {
433        Self {
434            returned: Some(H1Returned {
435                http_sender,
436                return_tx,
437            }),
438        }
439    }
440
441    fn from_returned(returned: H1Returned<B>) -> Self {
442        Self {
443            returned: Some(returned),
444        }
445    }
446
447    fn is_ready(&self) -> bool {
448        self.returned
449            .as_ref()
450            .is_some_and(|returned| returned.http_sender.is_ready())
451    }
452
453    async fn send_request(
454        &mut self,
455        req: Request<B>,
456    ) -> hyper::Result<http::Response<hyper::body::Incoming>>
457    where
458        B: http_body::Body + Send + 'static,
459        B::Data: Send,
460        B::Error: Into<BoxError> + 'static,
461    {
462        // Once the sender is taken, this old lease can never re-enter the pool.
463        let returned = self
464            .returned
465            .take()
466            .expect("an HTTP/1 lease can only send one request");
467
468        let mut guard = H1ReturnGuard {
469            returned: Some(returned),
470        };
471
472        let result = guard.http_sender_mut().send_request(req).await;
473
474        // Return the sender before the response is handed to the caller.
475        guard.return_to_driver();
476        result
477    }
478}
479
480#[cfg(feature = "http1")]
481#[pin_project::pin_project]
482struct ManagedH1Connection<I, B>
483where
484    I: hyper::rt::Read + hyper::rt::Write,
485    B: http_body::Body + Send + 'static,
486{
487    #[pin]
488    connection: conn::http1::Connection<I, B>,
489    return_rx: tokio::sync::mpsc::UnboundedReceiver<H1Returned<B>>,
490    waiting: Option<H1Returned<B>>,
491    returner: pool::PoolReturn<PoolKey, HttpConnection<B>>,
492    key: PoolKey,
493}
494
495#[cfg(feature = "http1")]
496impl<I, B> Future for ManagedH1Connection<I, B>
497where
498    I: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static,
499    B: http_body::Body + Unpin + Send + 'static,
500    B::Data: Send,
501    B::Error: Into<BoxError> + 'static,
502{
503    type Output = ();
504
505    fn poll(
506        self: std::pin::Pin<&mut Self>,
507        cx: &mut std::task::Context<'_>,
508    ) -> std::task::Poll<Self::Output> {
509        use std::task::Poll;
510
511        let this = self.project();
512
513        // 1. Receive the sender returned by the request future.
514        if this.waiting.is_none() {
515            match this.return_rx.poll_recv(cx) {
516                Poll::Ready(Some(returned)) => {
517                    *this.waiting = Some(returned);
518                }
519                Poll::Ready(None) => {
520                    // No lease, guard, queued message, or waiting sender remains.
521                    tracing::trace!("HTTP/1 return channel closed");
522                    return Poll::Ready(());
523                }
524                Poll::Pending => {}
525            }
526        }
527
528        // 2. Drive the actual HTTP/1 socket I/O and state machine.
529        match this.connection.poll(cx) {
530            Poll::Ready(Ok(())) => {
531                tracing::trace!("HTTP/1 connection driver completed");
532                return Poll::Ready(());
533            }
534            Poll::Ready(Err(err)) => {
535                tracing::trace!("HTTP/1 connection driver failed: {err}");
536                return Poll::Ready(());
537            }
538            Poll::Pending => {}
539        }
540
541        // 3. If no sender has arrived, only the connection needs driving.
542        let Some(returned) = this.waiting.as_mut() else {
543            // Reaching this branch means poll_recv returned Pending above and
544            // registered cx.waker(). Connection::poll also returned Pending,
545            // so either the return channel or Hyper I/O will wake this task.
546            return Poll::Pending;
547        };
548
549        // 4. Wait until Hyper explicitly reports that the sender is reusable.
550        // poll_ready also registers this driver's waker when it returns Pending.
551        match returned.http_sender.poll_ready(cx) {
552            Poll::Pending => return Poll::Pending,
553            Poll::Ready(Err(err)) => {
554                tracing::trace!("HTTP/1 sender closed before becoming reusable: {err}");
555                return Poll::Ready(());
556            }
557            Poll::Ready(Ok(())) => {}
558        }
559
560        // 5. Once truly ready, create the next lease and return it to the pool.
561        let returned = this
562            .waiting
563            .take()
564            .expect("HTTP/1 waiting sender disappeared");
565        let http_connection = HttpConnection::H1(H1Lease::from_returned(returned));
566
567        if let Err(_err) = this.returner.put_ready(this.key.clone(), http_connection) {
568            // The response may have completed successfully, but the pool no longer
569            // exists. Drop the returned sender and stop driving this connection.
570            tracing::trace!("HTTP/1 pool dropped before the connection became reusable");
571            return Poll::Ready(());
572        }
573
574        // 6. Re-register the receiver waker for the next return or for the last
575        // tx being dropped.
576        match this.return_rx.poll_recv(cx) {
577            Poll::Ready(Some(returned)) => {
578                debug_assert!(this.waiting.is_none());
579                *this.waiting = Some(returned);
580                cx.waker().wake_by_ref();
581                Poll::Pending
582            }
583            Poll::Ready(None) => {
584                // For example, max_idle_per_host=0 drops the new lease at once.
585                Poll::Ready(())
586            }
587            Poll::Pending => Poll::Pending,
588        }
589    }
590}
591
592enum HttpConnection<B> {
593    #[cfg(feature = "http1")]
594    H1(H1Lease<B>),
595    #[cfg(feature = "http2")]
596    H2(conn::http2::SendRequest<B>),
597}
598
599impl<B> Poolable for HttpConnection<B>
600where
601    B: Send + 'static,
602{
603    fn is_open(&self) -> bool {
604        match &self {
605            #[cfg(feature = "http1")]
606            Self::H1(h1) => h1.is_ready(),
607            #[cfg(feature = "http2")]
608            Self::H2(h2) => h2.is_ready(),
609        }
610    }
611
612    fn reserve(self) -> Reservation<Self> {
613        match self {
614            #[cfg(feature = "http1")]
615            Self::H1(h1) => Reservation::Unique(Self::H1(h1)),
616            #[cfg(feature = "http2")]
617            Self::H2(h2) => Reservation::Shared(Self::H2(h2.clone()), Self::H2(h2)),
618        }
619    }
620
621    fn can_share(&self) -> bool {
622        match self {
623            #[cfg(feature = "http1")]
624            Self::H1(_) => false,
625            #[cfg(feature = "http2")]
626            Self::H2(_) => true,
627        }
628    }
629}
630
631impl<B> HttpConnection<B>
632where
633    B: http_body::Body + Send + 'static,
634    B::Data: Send,
635    B::Error: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
636{
637    pub async fn send_request(&mut self, req: Request<B>) -> Result<Response> {
638        let res = match self {
639            #[cfg(feature = "http1")]
640            Self::H1(h1) => h1.send_request(req).await,
641            #[cfg(feature = "http2")]
642            Self::H2(h2) => h2.send_request(req).await,
643        };
644        match res {
645            Ok(resp) => Ok(resp.map(Body::from_incoming)),
646            Err(err) => Err(request_error(err)),
647        }
648    }
649}
650
651static PLACEHOLDER: LazyLock<Authority> =
652    LazyLock::new(|| Authority::from_static("volo-http.placeholder"));
653
654fn gen_authority<B>(req: &Request<B>) -> Authority {
655    let Some(host) = req.headers().get(header::HOST) else {
656        return PLACEHOLDER.to_owned();
657    };
658    let Ok(host) = host.to_str() else {
659        return PLACEHOLDER.to_owned();
660    };
661    let Ok(authority) = Authority::from_str(host) else {
662        return PLACEHOLDER.to_owned();
663    };
664    authority
665}
666
667// We use this function for HTTP/2 only because
668//
669// 1. header of http2 request has a field `:scheme`, hyper demands that uri of h2 request MUST have
670//    FULL uri, althrough scheme in `Uri` is optional, but authority is required.
671//
672//    If authority exists, hyper will set `:scheme` to HTTP if there is no scheme in `Uri`. But if
673//    there is no authority, hyper will throw an error `MissingUriSchemeAndAuthority`.
674//
675// 2. For http2 request, hyper will ignore `Host` in `HeaderMap` and take authority as its `Host` in
676//    HEADERS frame. So we must take our `Host` and set it as authority of `Uri`.
677fn rewrite_uri<B>(cx: &ClientContext, req: &mut Request<B>) {
678    if req.version() != Version::HTTP_2 {
679        return;
680    }
681    let scheme = cx.target().scheme().cloned().unwrap_or(Scheme::HTTP);
682    let authority = gen_authority(req);
683    let mut parts = req.uri().to_owned().into_parts();
684    parts.scheme = Some(scheme);
685    parts.authority = Some(authority);
686    let Ok(uri) = Uri::from_parts(parts) else {
687        return;
688    };
689    *req.uri_mut() = uri;
690}
691
692#[cfg(all(test, feature = "http1"))]
693mod h1_connection_reuse_tests {
694    use std::{
695        future::{Future, pending, poll_fn},
696        net::{Ipv4Addr, SocketAddr},
697        task::Poll,
698        time::Duration,
699    };
700
701    use bytes::Bytes;
702    use http::{Request, header::HOST, uri::Scheme};
703    use http_body_util::Empty;
704    use hyper::client::conn;
705    use hyper_util::rt::TokioIo;
706    use tokio::{
707        io::{AsyncReadExt, AsyncWriteExt, DuplexStream},
708        sync::{mpsc, oneshot},
709        time::timeout,
710    };
711    use volo::net::Address;
712
713    use super::{H1Lease, HttpConnection, ManagedH1Connection, PoolKey, pool};
714    use crate::body::BodyConversion;
715
716    fn pool_key() -> PoolKey {
717        (
718            Scheme::HTTP,
719            Address::Ip(SocketAddr::from((Ipv4Addr::LOCALHOST, 80))),
720            #[cfg(feature = "__tls")]
721            None,
722        )
723    }
724
725    fn empty_request() -> Request<Empty<Bytes>> {
726        Request::builder()
727            .method("GET")
728            .uri("/")
729            .header(HOST, "example.test")
730            .body(Empty::new())
731            .expect("valid request")
732    }
733
734    async fn read_request_head(io: &mut DuplexStream) {
735        let mut head = Vec::new();
736        let mut byte = [0_u8; 1];
737
738        loop {
739            let read = io.read(&mut byte).await.expect("read request");
740            assert_ne!(read, 0, "client closed before sending a full request");
741            head.push(byte[0]);
742
743            if head.ends_with(b"\r\n\r\n") {
744                return;
745            }
746        }
747    }
748
749    #[tokio::test]
750    async fn managed_driver_reuses_only_after_response_body_eof() {
751        let (client_io, mut server_io) = tokio::io::duplex(4096);
752        let (head_sent_tx, head_sent_rx) = oneshot::channel();
753        let (release_body_tx, release_body_rx) = oneshot::channel();
754
755        let server_task = tokio::spawn(async move {
756            read_request_head(&mut server_io).await;
757            server_io
758                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\n")
759                .await
760                .expect("write first response head");
761            head_sent_tx.send(()).expect("signal response head");
762
763            release_body_rx.await.expect("release first response body");
764            server_io
765                .write_all(b"pong")
766                .await
767                .expect("write first response body");
768
769            // A second request on the same DuplexStream proves physical reuse.
770            read_request_head(&mut server_io).await;
771            server_io
772                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
773                .await
774                .expect("write second response");
775
776            pending::<()>().await;
777        });
778
779        let (mut sender, connection) =
780            conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(client_io))
781                .await
782                .expect("HTTP/1 handshake");
783
784        let pool = pool::Pool::new(pool::Config {
785            idle_timeout: Duration::from_secs(60),
786            max_idle_per_host: 16,
787        });
788        let key = pool_key();
789        let (return_tx, return_rx) = mpsc::unbounded_channel();
790
791        let driver_task = tokio::spawn(ManagedH1Connection {
792            connection,
793            return_rx,
794            waiting: None,
795            returner: pool.return_handle(),
796            key: key.clone(),
797        });
798
799        sender.ready().await.expect("initial sender readiness");
800        let connecting = pool
801            .connecting(&key, pool::Ver::Auto)
802            .expect("HTTP/1 does not serialize connects");
803        let mut pooled = pool.pooled(
804            connecting,
805            HttpConnection::H1(H1Lease::new(sender, return_tx)),
806        );
807
808        let response = pooled
809            .send_request(empty_request())
810            .await
811            .expect("first response head");
812        head_sent_rx.await.expect("server sent response head");
813
814        // Poll checkout exactly once. The body is still blocked by the server,
815        // so returning a connection here would be premature reuse.
816        let mut early_checkout = Box::pin(pool.checkout(key.clone()));
817        let returned_too_early =
818            poll_fn(|cx| Poll::Ready(matches!(early_checkout.as_mut().poll(cx), Poll::Ready(_))))
819                .await;
820        assert!(!returned_too_early);
821        drop(early_checkout);
822
823        release_body_tx.send(()).expect("release response body");
824        assert_eq!(
825            response
826                .into_body()
827                .into_vec()
828                .await
829                .expect("collect first response"),
830            b"pong"
831        );
832
833        // The old Pooled contains an empty H1Lease and must not reinsert itself.
834        drop(pooled);
835
836        let mut reused = timeout(Duration::from_secs(1), pool.checkout(key.clone()))
837            .await
838            .expect("driver did not return the ready connection")
839            .expect("checkout failed");
840
841        let second = reused
842            .send_request(empty_request())
843            .await
844            .expect("second response head");
845        assert_eq!(
846            second
847                .into_body()
848                .into_vec()
849                .await
850                .expect("collect second response"),
851            b"ok"
852        );
853
854        drop(reused);
855        drop(pool);
856
857        timeout(Duration::from_secs(1), driver_task)
858            .await
859            .expect("driver should stop after the pool is dropped")
860            .expect("driver task panicked");
861        server_task.abort();
862    }
863
864    #[tokio::test]
865    async fn canceled_send_returns_sender_and_consumes_old_lease() {
866        let (client_io, mut server_io) = tokio::io::duplex(4096);
867        let (request_seen_tx, request_seen_rx) = oneshot::channel();
868
869        let server_task = tokio::spawn(async move {
870            read_request_head(&mut server_io).await;
871            request_seen_tx.send(()).expect("signal request received");
872
873            // Keep the request future waiting for a response head.
874            pending::<()>().await;
875        });
876
877        let (mut sender, connection) =
878            conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(client_io))
879                .await
880                .expect("HTTP/1 handshake");
881        let connection_task = tokio::spawn(async move {
882            let _ = connection.await;
883        });
884
885        sender.ready().await.expect("initial sender readiness");
886        let (return_tx, mut return_rx) = mpsc::unbounded_channel();
887        let mut lease = H1Lease::new(sender, return_tx);
888        let mut send = Box::pin(lease.send_request(empty_request()));
889
890        tokio::select! {
891            result = &mut send => panic!("request completed unexpectedly: {result:?}"),
892            result = request_seen_rx => result.expect("server observed the request"),
893        }
894
895        // Dropping the future must run H1ReturnGuard::drop.
896        drop(send);
897
898        assert!(
899            lease.returned.is_none(),
900            "the old lease must permanently lose its sender"
901        );
902
903        let returned = timeout(Duration::from_secs(1), return_rx.recv())
904            .await
905            .expect("guard did not return the sender")
906            .expect("return channel closed unexpectedly");
907
908        assert!(matches!(
909            return_rx.try_recv(),
910            Err(mpsc::error::TryRecvError::Empty)
911        ));
912
913        drop(returned);
914        connection_task.abort();
915        server_task.abort();
916    }
917
918    #[tokio::test]
919    async fn managed_driver_stops_when_return_channel_is_closed() {
920        let (client_io, _server_io) = tokio::io::duplex(4096);
921        let (_sender, connection) =
922            conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(client_io))
923                .await
924                .expect("HTTP/1 handshake");
925
926        let pool = pool::Pool::new(pool::Config {
927            idle_timeout: Duration::from_secs(60),
928            max_idle_per_host: 16,
929        });
930        let (return_tx, return_rx) = mpsc::unbounded_channel();
931        drop(return_tx); // close tx
932
933        let driver = ManagedH1Connection {
934            connection,
935            return_rx,
936            waiting: None,
937            returner: pool.return_handle(),
938            key: pool_key(),
939        };
940
941        timeout(Duration::from_secs(1), driver)
942            .await
943            .expect("driver should observe the closed return channel");
944    }
945
946    #[tokio::test]
947    async fn managed_driver_stops_when_connection_completes() {
948        let (client_io, _server_io) = tokio::io::duplex(4096);
949        let (sender, connection) =
950            conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(client_io))
951                .await
952                .expect("HTTP/1 handshake");
953
954        let pool = pool::Pool::new(pool::Config {
955            idle_timeout: Duration::from_secs(60),
956            max_idle_per_host: 16,
957        });
958        let (_return_tx, return_rx) = mpsc::unbounded_channel();
959        let driver = ManagedH1Connection {
960            connection,
961            return_rx,
962            waiting: None,
963            returner: pool.return_handle(),
964            key: pool_key(),
965        };
966
967        drop(sender);
968
969        timeout(Duration::from_secs(1), driver)
970            .await
971            .expect("dropping the Hyper sender should complete the connection driver");
972    }
973
974    #[tokio::test]
975    async fn managed_driver_stops_when_connection_fails() {
976        let io = tokio_test::io::Builder::new()
977            .read_error(std::io::Error::other("injected read failure"))
978            .build(); // build: read error
979        let (sender, connection) = conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(io))
980            .await
981            .expect("HTTP/1 handshake");
982
983        let pool = pool::Pool::new(pool::Config {
984            idle_timeout: Duration::from_secs(60),
985            max_idle_per_host: 16,
986        });
987        let (return_tx, return_rx) = mpsc::unbounded_channel();
988        let driver = ManagedH1Connection {
989            connection,
990            return_rx,
991            waiting: None,
992            returner: pool.return_handle(),
993            key: pool_key(),
994        };
995
996        timeout(Duration::from_secs(1), driver)
997            .await
998            .expect("I/O failure should stop the connection driver");
999
1000        drop(sender);
1001        drop(return_tx);
1002    }
1003
1004    #[tokio::test]
1005    async fn managed_driver_drops_sender_closed_before_reuse() {
1006        let (client_a, _server_a) = tokio::io::duplex(4096);
1007        let (sender_a, connection_a) =
1008            conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(client_a))
1009                .await
1010                .expect("first HTTP/1 handshake");
1011
1012        let (client_b, _server_b) = tokio::io::duplex(4096);
1013        let (sender_b, connection_b) =
1014            conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(client_b))
1015                .await
1016                .expect("second HTTP/1 handshake");
1017        drop(connection_b);
1018
1019        let pool = pool::Pool::new(pool::Config {
1020            idle_timeout: Duration::from_secs(60),
1021            max_idle_per_host: 16,
1022        });
1023        let key = pool_key();
1024        let (return_tx, return_rx) = mpsc::unbounded_channel();
1025        let driver = ManagedH1Connection {
1026            connection: connection_a,
1027            return_rx,
1028            waiting: Some(crate::client::protocol::H1Returned {
1029                http_sender: sender_b,
1030                return_tx: return_tx.clone(),
1031            }),
1032            returner: pool.return_handle(),
1033            key: key.clone(),
1034        };
1035
1036        timeout(Duration::from_secs(1), driver)
1037            .await
1038            .expect("closed sender should stop the driver");
1039
1040        let mut checkout = pool.checkout(key);
1041        let returned = poll_fn(|cx| {
1042            Poll::Ready(matches!(
1043                std::pin::Pin::new(&mut checkout).poll(cx),
1044                Poll::Ready(_)
1045            ))
1046        })
1047        .await;
1048        assert!(!returned, "closed sender must not be returned to the pool");
1049
1050        drop(sender_a);
1051        drop(return_tx);
1052    }
1053}