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    use_h2 && (ver == pool::Ver::Http2 || cfg!(not(feature = "http1")))
325}
326
327impl<B> Service<ClientContext, Request<B>> for ClientTransport<B>
328where
329    B: http_body::Body + Unpin + Send + 'static,
330    B::Data: Send,
331    B::Error: Into<Box<dyn Error + Send + Sync>> + 'static,
332{
333    type Response = Response;
334    type Error = ClientError;
335
336    async fn call(
337        &self,
338        cx: &mut ClientContext,
339        mut req: Request<B>,
340    ) -> Result<Self::Response, Self::Error> {
341        rewrite_uri(cx, &mut req);
342
343        let callee = cx.rpc_info().callee();
344        let address = callee.address().ok_or_else(no_address)?;
345
346        let ver = req.version();
347        let peer = PeerInfo {
348            scheme: cx.target().scheme().cloned().unwrap_or(Scheme::HTTP),
349            address,
350            #[cfg(feature = "__tls")]
351            name: callee.service_name(),
352        };
353
354        let stat_enabled = self.config.stat_enable;
355        if stat_enabled {
356            cx.stats.record_transport_start_at();
357        }
358
359        let mut conn = tri!(self.pooled_connect(ver, peer).await);
360        let res = conn.send_request(req).await;
361
362        if stat_enabled {
363            cx.stats.record_transport_end_at();
364        }
365
366        res
367    }
368}
369
370#[cfg(feature = "http1")]
371struct H1Returned<B> {
372    http_sender: conn::http1::SendRequest<B>,
373    return_tx: tokio::sync::mpsc::UnboundedSender<H1Returned<B>>,
374}
375
376#[cfg(feature = "http1")]
377struct H1Lease<B> {
378    returned: Option<H1Returned<B>>,
379}
380
381#[cfg(feature = "http1")]
382struct H1ReturnGuard<B> {
383    returned: Option<H1Returned<B>>,
384}
385
386#[cfg(feature = "http1")]
387impl<B> H1ReturnGuard<B> {
388    fn http_sender_mut(&mut self) -> &mut conn::http1::SendRequest<B> {
389        &mut self
390            .returned
391            .as_mut()
392            .expect("HTTP/1 sender already returned")
393            .http_sender
394    }
395
396    fn return_to_driver(&mut self) {
397        let Some(returned) = self.returned.take() else {
398            return;
399        };
400
401        // The original tx must keep moving with the message. Use a temporary
402        // clone to perform this send.
403        let tx = returned.return_tx.clone();
404        if let Err(_err) = tx.send(returned) {
405            // The receiver is gone, so the driver for this physical connection
406            // has already stopped. There is no receiver to retry, and a sender
407            // whose readiness is unknown must not be returned directly to the
408            // pool. Dropping SendError also drops the sender and return_tx,
409            // explicitly giving up reuse of this connection.
410            tracing::trace!("HTTP/1 connection driver already closed");
411        };
412    }
413}
414
415#[cfg(feature = "http1")]
416impl<B> Drop for H1ReturnGuard<B> {
417    fn drop(&mut self) {
418        // Cancellation of the send_request future uses the same return path.
419        self.return_to_driver();
420    }
421}
422
423#[cfg(feature = "http1")]
424impl<B> H1Lease<B> {
425    fn new(
426        http_sender: conn::http1::SendRequest<B>,
427        return_tx: tokio::sync::mpsc::UnboundedSender<H1Returned<B>>,
428    ) -> Self {
429        Self {
430            returned: Some(H1Returned {
431                http_sender,
432                return_tx,
433            }),
434        }
435    }
436
437    fn from_returned(returned: H1Returned<B>) -> Self {
438        Self {
439            returned: Some(returned),
440        }
441    }
442
443    fn is_ready(&self) -> bool {
444        self.returned
445            .as_ref()
446            .is_some_and(|returned| returned.http_sender.is_ready())
447    }
448
449    async fn send_request(
450        &mut self,
451        req: Request<B>,
452    ) -> hyper::Result<http::Response<hyper::body::Incoming>>
453    where
454        B: http_body::Body + Send + 'static,
455        B::Data: Send,
456        B::Error: Into<BoxError> + 'static,
457    {
458        // Once the sender is taken, this old lease can never re-enter the pool.
459        let returned = self
460            .returned
461            .take()
462            .expect("an HTTP/1 lease can only send one request");
463
464        let mut guard = H1ReturnGuard {
465            returned: Some(returned),
466        };
467
468        let result = guard.http_sender_mut().send_request(req).await;
469
470        // Return the sender before the response is handed to the caller.
471        guard.return_to_driver();
472        result
473    }
474}
475
476#[cfg(feature = "http1")]
477#[pin_project::pin_project]
478struct ManagedH1Connection<I, B>
479where
480    I: hyper::rt::Read + hyper::rt::Write,
481    B: http_body::Body + Send + 'static,
482{
483    #[pin]
484    connection: conn::http1::Connection<I, B>,
485    return_rx: tokio::sync::mpsc::UnboundedReceiver<H1Returned<B>>,
486    waiting: Option<H1Returned<B>>,
487    returner: pool::PoolReturn<PoolKey, HttpConnection<B>>,
488    key: PoolKey,
489}
490
491#[cfg(feature = "http1")]
492impl<I, B> Future for ManagedH1Connection<I, B>
493where
494    I: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static,
495    B: http_body::Body + Unpin + Send + 'static,
496    B::Data: Send,
497    B::Error: Into<BoxError> + 'static,
498{
499    type Output = ();
500
501    fn poll(
502        self: std::pin::Pin<&mut Self>,
503        cx: &mut std::task::Context<'_>,
504    ) -> std::task::Poll<Self::Output> {
505        use std::task::Poll;
506
507        let this = self.project();
508
509        // 1. Receive the sender returned by the request future.
510        if this.waiting.is_none() {
511            match this.return_rx.poll_recv(cx) {
512                Poll::Ready(Some(returned)) => {
513                    *this.waiting = Some(returned);
514                }
515                Poll::Ready(None) => {
516                    // No lease, guard, queued message, or waiting sender remains.
517                    tracing::trace!("HTTP/1 return channel closed");
518                    return Poll::Ready(());
519                }
520                Poll::Pending => {}
521            }
522        }
523
524        // 2. Drive the actual HTTP/1 socket I/O and state machine.
525        match this.connection.poll(cx) {
526            Poll::Ready(Ok(())) => {
527                tracing::trace!("HTTP/1 connection driver completed");
528                return Poll::Ready(());
529            }
530            Poll::Ready(Err(err)) => {
531                tracing::trace!("HTTP/1 connection driver failed: {err}");
532                return Poll::Ready(());
533            }
534            Poll::Pending => {}
535        }
536
537        // 3. If no sender has arrived, only the connection needs driving.
538        let Some(returned) = this.waiting.as_mut() else {
539            // Reaching this branch means poll_recv returned Pending above and
540            // registered cx.waker(). Connection::poll also returned Pending,
541            // so either the return channel or Hyper I/O will wake this task.
542            return Poll::Pending;
543        };
544
545        // 4. Wait until Hyper explicitly reports that the sender is reusable.
546        // poll_ready also registers this driver's waker when it returns Pending.
547        match returned.http_sender.poll_ready(cx) {
548            Poll::Pending => return Poll::Pending,
549            Poll::Ready(Err(err)) => {
550                tracing::trace!("HTTP/1 sender closed before becoming reusable: {err}");
551                return Poll::Ready(());
552            }
553            Poll::Ready(Ok(())) => {}
554        }
555
556        // 5. Once truly ready, create the next lease and return it to the pool.
557        let returned = this
558            .waiting
559            .take()
560            .expect("HTTP/1 waiting sender disappeared");
561        let http_connection = HttpConnection::H1(H1Lease::from_returned(returned));
562
563        if let Err(_err) = this.returner.put_ready(this.key.clone(), http_connection) {
564            // The response may have completed successfully, but the pool no longer
565            // exists. Drop the returned sender and stop driving this connection.
566            tracing::trace!("HTTP/1 pool dropped before the connection became reusable");
567            return Poll::Ready(());
568        }
569
570        // 6. Re-register the receiver waker for the next return or for the last
571        // tx being dropped.
572        match this.return_rx.poll_recv(cx) {
573            Poll::Ready(Some(returned)) => {
574                debug_assert!(this.waiting.is_none());
575                *this.waiting = Some(returned);
576                cx.waker().wake_by_ref();
577                Poll::Pending
578            }
579            Poll::Ready(None) => {
580                // For example, max_idle_per_host=0 drops the new lease at once.
581                Poll::Ready(())
582            }
583            Poll::Pending => Poll::Pending,
584        }
585    }
586}
587
588enum HttpConnection<B> {
589    #[cfg(feature = "http1")]
590    H1(H1Lease<B>),
591    #[cfg(feature = "http2")]
592    H2(conn::http2::SendRequest<B>),
593}
594
595impl<B> Poolable for HttpConnection<B>
596where
597    B: Send + 'static,
598{
599    fn is_open(&self) -> bool {
600        match &self {
601            #[cfg(feature = "http1")]
602            Self::H1(h1) => h1.is_ready(),
603            #[cfg(feature = "http2")]
604            Self::H2(h2) => h2.is_ready(),
605        }
606    }
607
608    fn reserve(self) -> Reservation<Self> {
609        match self {
610            #[cfg(feature = "http1")]
611            Self::H1(h1) => Reservation::Unique(Self::H1(h1)),
612            #[cfg(feature = "http2")]
613            Self::H2(h2) => Reservation::Shared(Self::H2(h2.clone()), Self::H2(h2)),
614        }
615    }
616
617    fn can_share(&self) -> bool {
618        match self {
619            #[cfg(feature = "http1")]
620            Self::H1(_) => false,
621            #[cfg(feature = "http2")]
622            Self::H2(_) => true,
623        }
624    }
625}
626
627impl<B> HttpConnection<B>
628where
629    B: http_body::Body + Send + 'static,
630    B::Data: Send,
631    B::Error: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
632{
633    pub async fn send_request(&mut self, req: Request<B>) -> Result<Response> {
634        let res = match self {
635            #[cfg(feature = "http1")]
636            Self::H1(h1) => h1.send_request(req).await,
637            #[cfg(feature = "http2")]
638            Self::H2(h2) => h2.send_request(req).await,
639        };
640        match res {
641            Ok(resp) => Ok(resp.map(Body::from_incoming)),
642            Err(err) => Err(request_error(err)),
643        }
644    }
645}
646
647static PLACEHOLDER: LazyLock<Authority> =
648    LazyLock::new(|| Authority::from_static("volo-http.placeholder"));
649
650fn gen_authority<B>(req: &Request<B>) -> Authority {
651    let Some(host) = req.headers().get(header::HOST) else {
652        return PLACEHOLDER.to_owned();
653    };
654    let Ok(host) = host.to_str() else {
655        return PLACEHOLDER.to_owned();
656    };
657    let Ok(authority) = Authority::from_str(host) else {
658        return PLACEHOLDER.to_owned();
659    };
660    authority
661}
662
663// We use this function for HTTP/2 only because
664//
665// 1. header of http2 request has a field `:scheme`, hyper demands that uri of h2 request MUST have
666//    FULL uri, althrough scheme in `Uri` is optional, but authority is required.
667//
668//    If authority exists, hyper will set `:scheme` to HTTP if there is no scheme in `Uri`. But if
669//    there is no authority, hyper will throw an error `MissingUriSchemeAndAuthority`.
670//
671// 2. For http2 request, hyper will ignore `Host` in `HeaderMap` and take authority as its `Host` in
672//    HEADERS frame. So we must take our `Host` and set it as authority of `Uri`.
673fn rewrite_uri<B>(cx: &ClientContext, req: &mut Request<B>) {
674    if req.version() != Version::HTTP_2 {
675        return;
676    }
677    let scheme = cx.target().scheme().cloned().unwrap_or(Scheme::HTTP);
678    let authority = gen_authority(req);
679    let mut parts = req.uri().to_owned().into_parts();
680    parts.scheme = Some(scheme);
681    parts.authority = Some(authority);
682    let Ok(uri) = Uri::from_parts(parts) else {
683        return;
684    };
685    *req.uri_mut() = uri;
686}
687
688#[cfg(all(test, feature = "http1"))]
689mod h1_connection_reuse_tests {
690    use std::{
691        future::{Future, pending, poll_fn},
692        net::{Ipv4Addr, SocketAddr},
693        task::Poll,
694        time::Duration,
695    };
696
697    use bytes::Bytes;
698    use http::{Request, header::HOST, uri::Scheme};
699    use http_body_util::Empty;
700    use hyper::client::conn;
701    use hyper_util::rt::TokioIo;
702    use tokio::{
703        io::{AsyncReadExt, AsyncWriteExt, DuplexStream},
704        sync::{mpsc, oneshot},
705        time::timeout,
706    };
707    use volo::net::Address;
708
709    use super::{H1Lease, HttpConnection, ManagedH1Connection, PoolKey, pool};
710    use crate::body::BodyConversion;
711
712    fn pool_key() -> PoolKey {
713        (
714            Scheme::HTTP,
715            Address::Ip(SocketAddr::from((Ipv4Addr::LOCALHOST, 80))),
716            #[cfg(feature = "__tls")]
717            None,
718        )
719    }
720
721    fn empty_request() -> Request<Empty<Bytes>> {
722        Request::builder()
723            .method("GET")
724            .uri("/")
725            .header(HOST, "example.test")
726            .body(Empty::new())
727            .expect("valid request")
728    }
729
730    async fn read_request_head(io: &mut DuplexStream) {
731        let mut head = Vec::new();
732        let mut byte = [0_u8; 1];
733
734        loop {
735            let read = io.read(&mut byte).await.expect("read request");
736            assert_ne!(read, 0, "client closed before sending a full request");
737            head.push(byte[0]);
738
739            if head.ends_with(b"\r\n\r\n") {
740                return;
741            }
742        }
743    }
744
745    #[tokio::test]
746    async fn managed_driver_reuses_only_after_response_body_eof() {
747        let (client_io, mut server_io) = tokio::io::duplex(4096);
748        let (head_sent_tx, head_sent_rx) = oneshot::channel();
749        let (release_body_tx, release_body_rx) = oneshot::channel();
750
751        let server_task = tokio::spawn(async move {
752            read_request_head(&mut server_io).await;
753            server_io
754                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\n")
755                .await
756                .expect("write first response head");
757            head_sent_tx.send(()).expect("signal response head");
758
759            release_body_rx.await.expect("release first response body");
760            server_io
761                .write_all(b"pong")
762                .await
763                .expect("write first response body");
764
765            // A second request on the same DuplexStream proves physical reuse.
766            read_request_head(&mut server_io).await;
767            server_io
768                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
769                .await
770                .expect("write second response");
771
772            pending::<()>().await;
773        });
774
775        let (mut sender, connection) =
776            conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(client_io))
777                .await
778                .expect("HTTP/1 handshake");
779
780        let pool = pool::Pool::new(pool::Config {
781            idle_timeout: Duration::from_secs(60),
782            max_idle_per_host: 16,
783        });
784        let key = pool_key();
785        let (return_tx, return_rx) = mpsc::unbounded_channel();
786
787        let driver_task = tokio::spawn(ManagedH1Connection {
788            connection,
789            return_rx,
790            waiting: None,
791            returner: pool.return_handle(),
792            key: key.clone(),
793        });
794
795        sender.ready().await.expect("initial sender readiness");
796        let connecting = pool
797            .connecting(&key, pool::Ver::Auto)
798            .expect("HTTP/1 does not serialize connects");
799        let mut pooled = pool.pooled(
800            connecting,
801            HttpConnection::H1(H1Lease::new(sender, return_tx)),
802        );
803
804        let response = pooled
805            .send_request(empty_request())
806            .await
807            .expect("first response head");
808        head_sent_rx.await.expect("server sent response head");
809
810        // Poll checkout exactly once. The body is still blocked by the server,
811        // so returning a connection here would be premature reuse.
812        let mut early_checkout = Box::pin(pool.checkout(key.clone()));
813        let returned_too_early =
814            poll_fn(|cx| Poll::Ready(matches!(early_checkout.as_mut().poll(cx), Poll::Ready(_))))
815                .await;
816        assert!(!returned_too_early);
817        drop(early_checkout);
818
819        release_body_tx.send(()).expect("release response body");
820        assert_eq!(
821            response
822                .into_body()
823                .into_vec()
824                .await
825                .expect("collect first response"),
826            b"pong"
827        );
828
829        // The old Pooled contains an empty H1Lease and must not reinsert itself.
830        drop(pooled);
831
832        let mut reused = timeout(Duration::from_secs(1), pool.checkout(key.clone()))
833            .await
834            .expect("driver did not return the ready connection")
835            .expect("checkout failed");
836
837        let second = reused
838            .send_request(empty_request())
839            .await
840            .expect("second response head");
841        assert_eq!(
842            second
843                .into_body()
844                .into_vec()
845                .await
846                .expect("collect second response"),
847            b"ok"
848        );
849
850        drop(reused);
851        drop(pool);
852
853        timeout(Duration::from_secs(1), driver_task)
854            .await
855            .expect("driver should stop after the pool is dropped")
856            .expect("driver task panicked");
857        server_task.abort();
858    }
859
860    #[tokio::test]
861    async fn canceled_send_returns_sender_and_consumes_old_lease() {
862        let (client_io, mut server_io) = tokio::io::duplex(4096);
863        let (request_seen_tx, request_seen_rx) = oneshot::channel();
864
865        let server_task = tokio::spawn(async move {
866            read_request_head(&mut server_io).await;
867            request_seen_tx.send(()).expect("signal request received");
868
869            // Keep the request future waiting for a response head.
870            pending::<()>().await;
871        });
872
873        let (mut sender, connection) =
874            conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(client_io))
875                .await
876                .expect("HTTP/1 handshake");
877        let connection_task = tokio::spawn(async move {
878            let _ = connection.await;
879        });
880
881        sender.ready().await.expect("initial sender readiness");
882        let (return_tx, mut return_rx) = mpsc::unbounded_channel();
883        let mut lease = H1Lease::new(sender, return_tx);
884        let mut send = Box::pin(lease.send_request(empty_request()));
885
886        tokio::select! {
887            result = &mut send => panic!("request completed unexpectedly: {result:?}"),
888            result = request_seen_rx => result.expect("server observed the request"),
889        }
890
891        // Dropping the future must run H1ReturnGuard::drop.
892        drop(send);
893
894        assert!(
895            lease.returned.is_none(),
896            "the old lease must permanently lose its sender"
897        );
898
899        let returned = timeout(Duration::from_secs(1), return_rx.recv())
900            .await
901            .expect("guard did not return the sender")
902            .expect("return channel closed unexpectedly");
903
904        assert!(matches!(
905            return_rx.try_recv(),
906            Err(mpsc::error::TryRecvError::Empty)
907        ));
908
909        drop(returned);
910        connection_task.abort();
911        server_task.abort();
912    }
913
914    #[tokio::test]
915    async fn managed_driver_stops_when_return_channel_is_closed() {
916        let (client_io, _server_io) = tokio::io::duplex(4096);
917        let (_sender, connection) =
918            conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(client_io))
919                .await
920                .expect("HTTP/1 handshake");
921
922        let pool = pool::Pool::new(pool::Config {
923            idle_timeout: Duration::from_secs(60),
924            max_idle_per_host: 16,
925        });
926        let (return_tx, return_rx) = mpsc::unbounded_channel();
927        drop(return_tx); // close tx
928
929        let driver = ManagedH1Connection {
930            connection,
931            return_rx,
932            waiting: None,
933            returner: pool.return_handle(),
934            key: pool_key(),
935        };
936
937        timeout(Duration::from_secs(1), driver)
938            .await
939            .expect("driver should observe the closed return channel");
940    }
941
942    #[tokio::test]
943    async fn managed_driver_stops_when_connection_completes() {
944        let (client_io, _server_io) = tokio::io::duplex(4096);
945        let (sender, connection) =
946            conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(client_io))
947                .await
948                .expect("HTTP/1 handshake");
949
950        let pool = pool::Pool::new(pool::Config {
951            idle_timeout: Duration::from_secs(60),
952            max_idle_per_host: 16,
953        });
954        let (_return_tx, return_rx) = mpsc::unbounded_channel();
955        let driver = ManagedH1Connection {
956            connection,
957            return_rx,
958            waiting: None,
959            returner: pool.return_handle(),
960            key: pool_key(),
961        };
962
963        drop(sender);
964
965        timeout(Duration::from_secs(1), driver)
966            .await
967            .expect("dropping the Hyper sender should complete the connection driver");
968    }
969
970    #[tokio::test]
971    async fn managed_driver_stops_when_connection_fails() {
972        let io = tokio_test::io::Builder::new()
973            .read_error(std::io::Error::other("injected read failure"))
974            .build(); // build: read error
975        let (sender, connection) = conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(io))
976            .await
977            .expect("HTTP/1 handshake");
978
979        let pool = pool::Pool::new(pool::Config {
980            idle_timeout: Duration::from_secs(60),
981            max_idle_per_host: 16,
982        });
983        let (return_tx, return_rx) = mpsc::unbounded_channel();
984        let driver = ManagedH1Connection {
985            connection,
986            return_rx,
987            waiting: None,
988            returner: pool.return_handle(),
989            key: pool_key(),
990        };
991
992        timeout(Duration::from_secs(1), driver)
993            .await
994            .expect("I/O failure should stop the connection driver");
995
996        drop(sender);
997        drop(return_tx);
998    }
999
1000    #[tokio::test]
1001    async fn managed_driver_drops_sender_closed_before_reuse() {
1002        let (client_a, _server_a) = tokio::io::duplex(4096);
1003        let (sender_a, connection_a) =
1004            conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(client_a))
1005                .await
1006                .expect("first HTTP/1 handshake");
1007
1008        let (client_b, _server_b) = tokio::io::duplex(4096);
1009        let (sender_b, connection_b) =
1010            conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(client_b))
1011                .await
1012                .expect("second HTTP/1 handshake");
1013        drop(connection_b);
1014
1015        let pool = pool::Pool::new(pool::Config {
1016            idle_timeout: Duration::from_secs(60),
1017            max_idle_per_host: 16,
1018        });
1019        let key = pool_key();
1020        let (return_tx, return_rx) = mpsc::unbounded_channel();
1021        let driver = ManagedH1Connection {
1022            connection: connection_a,
1023            return_rx,
1024            waiting: Some(crate::client::protocol::H1Returned {
1025                http_sender: sender_b,
1026                return_tx: return_tx.clone(),
1027            }),
1028            returner: pool.return_handle(),
1029            key: key.clone(),
1030        };
1031
1032        timeout(Duration::from_secs(1), driver)
1033            .await
1034            .expect("closed sender should stop the driver");
1035
1036        let mut checkout = pool.checkout(key);
1037        let returned = poll_fn(|cx| {
1038            Poll::Ready(matches!(
1039                std::pin::Pin::new(&mut checkout).poll(cx),
1040                Poll::Ready(_)
1041            ))
1042        })
1043        .await;
1044        assert!(!returned, "closed sender must not be returned to the pool");
1045
1046        drop(sender_a);
1047        drop(return_tx);
1048    }
1049}