1use std::{num::NonZeroUsize, time::Duration};
4
5use rama_core::Layer;
6use rama_core::error::{BoxError, BoxErrorExt as _};
7use rama_core::extensions::ExtensionsRef;
8use rama_net::address::{HostWithOptPort, ProxyAddress};
9use rama_net::client::pool::{ConnID, MultiplexPool, MuxSelection, PooledConnector, ReqToConnID};
10use rama_net::client::{ConnectRequest, ConnectorService, ConnectorTarget, ProxyRoute};
11use rama_net::{AuthorityInputExt, Protocol, ProtocolInputExt};
12
13use super::{BindBodyToConnLayer, BindBodyToConnector};
14
15#[derive(Clone, Debug, Default)]
16#[non_exhaustive]
17pub struct BasicHttpConnIdentifier;
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct BasicHttpConId {
25 pub protocol: Option<Protocol>,
26 pub authority: HostWithOptPort,
27 pub proxy_address: Option<ProxyAddress>,
28 pub connector_target: Option<ConnectorTarget>,
29}
30
31impl ConnID for BasicHttpConId {
32 #[cfg(feature = "opentelemetry")]
33 fn attributes(&self) -> impl Iterator<Item = rama_core::telemetry::opentelemetry::KeyValue> {
34 self.protocol
35 .as_ref()
36 .map(|protocol| {
37 rama_core::telemetry::opentelemetry::KeyValue::new("protocol", protocol.to_string())
38 })
39 .into_iter()
40 .chain([rama_core::telemetry::opentelemetry::KeyValue::new(
41 "authority",
42 self.authority.to_string(),
43 )])
44 }
45}
46
47impl<Input> ReqToConnID<Input> for BasicHttpConnIdentifier
48where
49 Input: AuthorityInputExt + ExtensionsRef + ProtocolInputExt,
50{
51 type ID = BasicHttpConId;
52
53 fn id(&self, req: &Input) -> Result<Self::ID, BoxError> {
54 let authority = req
55 .authority()
56 .ok_or_else(|| BoxError::from_static_str("no authority found in connection input"))?;
57 let protocol = req.protocol().cloned();
58
59 Ok(BasicHttpConId {
60 protocol,
61 authority,
62 proxy_address: req
63 .extensions()
64 .get_ref::<ProxyRoute>()
65 .and_then(ProxyRoute::proxy_address)
66 .cloned(),
67 connector_target: req.extensions().get_ref().cloned(),
68 })
69 }
70}
71
72pub type HttpPooledConnector<S> = BindBodyToConnector<
75 PooledConnector<
76 S,
77 MultiplexPool<<S as ConnectorService<ConnectRequest>>::Connection, BasicHttpConId>,
78 BasicHttpConnIdentifier,
79 >,
80>;
81
82#[derive(Debug, Clone)]
83pub struct HttpPooledConnectorConfig {
90 pub max_total: usize,
95 pub max_concurrent_streams: usize,
100 pub selection: MuxSelection,
102 pub idle_timeout: Option<Duration>,
105 pub wait_for_pool_timeout: Option<Duration>,
107}
108
109const DEFAULT_MAX_TOTAL: NonZeroUsize = NonZeroUsize::new(50).unwrap();
110const DEFAULT_MAX_CONCURRENT_STREAMS: NonZeroUsize = NonZeroUsize::new(100).unwrap();
111
112impl Default for HttpPooledConnectorConfig {
113 fn default() -> Self {
114 Self {
115 max_total: DEFAULT_MAX_TOTAL.get(),
116 max_concurrent_streams: DEFAULT_MAX_CONCURRENT_STREAMS.get(),
117 selection: MuxSelection::default(),
118 idle_timeout: Some(Duration::from_secs(300)),
119 wait_for_pool_timeout: Some(Duration::from_secs(120)),
120 }
121 }
122}
123
124impl HttpPooledConnectorConfig {
125 pub fn build_default_connector<S>(inner: S) -> HttpPooledConnector<S>
131 where
132 S: ConnectorService<ConnectRequest>,
133 {
134 let config = Self::default();
135 let pool = MultiplexPool::new(DEFAULT_MAX_CONCURRENT_STREAMS, DEFAULT_MAX_TOTAL)
136 .with_selection(config.selection)
137 .maybe_with_idle_timeout(config.idle_timeout);
138
139 let connector = PooledConnector::new(inner, pool, BasicHttpConnIdentifier)
140 .maybe_with_wait_for_pool_timeout(config.wait_for_pool_timeout);
141
142 BindBodyToConnLayer::new().into_layer(connector)
143 }
144
145 pub fn try_build_connector<S>(self, inner: S) -> Result<HttpPooledConnector<S>, BoxError>
160 where
161 S: ConnectorService<ConnectRequest>,
162 {
163 let pool = MultiplexPool::try_new(self.max_concurrent_streams, self.max_total)?
164 .with_selection(self.selection)
165 .maybe_with_idle_timeout(self.idle_timeout);
166
167 let connector = PooledConnector::new(inner, pool, BasicHttpConnIdentifier)
168 .maybe_with_wait_for_pool_timeout(self.wait_for_pool_timeout);
169
170 Ok(BindBodyToConnLayer::new().into_layer(connector))
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use std::convert::Infallible;
177 use std::sync::Arc;
178 use std::sync::atomic::{AtomicUsize, Ordering};
179 use std::time::Duration;
180
181 use rama_core::error::{BoxError, BoxErrorExt as _};
182 use rama_core::extensions::ExtensionsRef;
183 use rama_core::rt::Executor;
184 use rama_core::service::service_fn;
185 use rama_core::{Layer, Service, ServiceInput};
186 use rama_http_types::body::util::BodyExt as _;
187 use rama_http_types::{Body, HeaderValue, Request, Response, StatusCode, Version};
188 use rama_net::Protocol;
189 use rama_net::address::{HostWithPort, ProxyAddress};
190 use rama_net::client::pool::{MultiplexPool, PooledConnector, ReqToConnID};
191 use rama_net::client::{
192 ConnectRequest, ConnectionError, ConnectionErrorKind, ConnectorService,
193 EstablishedClientConnection, ProxyRoute, ProxyRoutes, ProxyRoutesConnector,
194 };
195 use rama_net::conn::{ConnectionHealth, ConnectionHealthWatcher};
196 use rama_net::test_utils::client::MockConnectorService;
197 use rama_utils::octets::kib;
198 use tokio::time::sleep;
199
200 use super::{BasicHttpConnIdentifier, HttpPooledConnector, HttpPooledConnectorConfig};
201 use crate::client::{HttpConnectRequestAdapter, HttpConnectorLayer};
202 use crate::server::HttpServer;
203
204 fn create_test_request(version: Version) -> Request {
205 Request::builder()
206 .uri("https://www.example.com")
207 .version(version)
208 .body(Body::from("a random request body"))
209 .unwrap()
210 }
211
212 fn build_test_connector<S>(
213 config: HttpPooledConnectorConfig,
214 inner: S,
215 ) -> HttpConnectRequestAdapter<HttpPooledConnector<S>>
216 where
217 S: ConnectorService<ConnectRequest>,
218 {
219 HttpConnectRequestAdapter::new(config.try_build_connector(inner).unwrap())
220 }
221
222 #[test]
223 fn connection_id_uses_only_the_selected_proxy_route() {
224 let request = Request::builder()
225 .uri("https://example.com")
226 .body(())
227 .unwrap();
228
229 request.extensions().insert(ProxyRoute::Direct);
230 let direct_id = BasicHttpConnIdentifier.id(&request).unwrap();
231 assert_eq!(direct_id.proxy_address, None);
232
233 let proxy_address = ProxyAddress {
234 protocol: Some(Protocol::HTTP),
235 address: HostWithPort::example_domain_http(),
236 credential: None,
237 };
238 request
239 .extensions()
240 .insert(ProxyRoute::Proxy(proxy_address.clone()));
241
242 let proxied_id = BasicHttpConnIdentifier.id(&request).unwrap();
243 assert_eq!(proxied_id.proxy_address, Some(proxy_address));
244 }
245
246 fn proxy_route(name: &str) -> ProxyRoute {
247 ProxyRoute::Proxy(
248 format!("http://{name}.example:8080")
249 .parse::<ProxyAddress>()
250 .unwrap(),
251 )
252 }
253
254 #[tokio::test]
255 async fn route_order_checks_pool_per_selected_route() {
256 let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
257 let inner = service_fn({
258 let attempts = attempts.clone();
259 move |input: ConnectRequest| {
260 let attempts = attempts.clone();
261 async move {
262 let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
263 let name = route
264 .proxy_address()
265 .map(|proxy| proxy.address.host.to_string())
266 .unwrap_or_else(|| "DIRECT".to_owned());
267 attempts.lock().push(name.clone());
268
269 if name == "c.example" {
270 Ok(EstablishedClientConnection {
271 input,
272 conn: ServiceInput::new(()),
273 })
274 } else {
275 Err(ConnectionError::transport(
276 BoxError::from_static_str("route unavailable"),
277 ConnectionErrorKind::Unavailable,
278 ))
279 }
280 }
281 }
282 });
283 let pool = MultiplexPool::try_new(10, 10).unwrap();
284 let pooled = PooledConnector::new(inner, pool, BasicHttpConnIdentifier);
285 let connector = ProxyRoutesConnector::new(pooled);
286
287 let first = ConnectRequest::new(HostWithPort::example_domain_https())
288 .with_application_protocol(Protocol::HTTPS);
289 first.extensions.insert(ProxyRoutes::new([
290 proxy_route("a"),
291 proxy_route("b"),
292 proxy_route("c"),
293 ]));
294 let established = connector.serve(first).await.unwrap();
295 assert_eq!(
296 established
297 .input
298 .extensions
299 .get_ref::<ProxyRoute>()
300 .unwrap(),
301 &proxy_route("c")
302 );
303 drop(established.conn);
304
305 let second = ConnectRequest::new(HostWithPort::example_domain_https())
308 .with_application_protocol(Protocol::HTTPS);
309 second
310 .extensions
311 .insert(ProxyRoutes::new([proxy_route("a"), proxy_route("c")]));
312 drop(connector.serve(second).await.unwrap().conn);
313
314 let third = ConnectRequest::new(HostWithPort::example_domain_https())
316 .with_application_protocol(Protocol::HTTPS);
317 third.extensions.insert(proxy_route("c"));
318 drop(connector.serve(third).await.unwrap().conn);
319
320 assert_eq!(
321 attempts.lock().as_slice(),
322 ["a.example", "b.example", "c.example", "a.example"]
323 );
324 }
325
326 fn tagging_mock_connector() -> impl ConnectorService<
332 ConnectRequest,
333 Connection: Service<Request, Output = Response, Error = BoxError> + ExtensionsRef,
334 > {
335 let conns = Arc::new(AtomicUsize::new(0));
336 HttpConnectorLayer::default().into_layer(MockConnectorService::new(move || {
337 let conn_id = conns.fetch_add(1, Ordering::Relaxed);
338 let resps = Arc::new(AtomicUsize::new(0));
339 HttpServer::auto(Executor::default()).service(service_fn(move |_req: Request| {
340 let resps = resps.clone();
341 async move {
342 let resp_id = resps.fetch_add(1, Ordering::Relaxed);
343 let mut resp = Response::new(Body::from("ok"));
344 let headers = resp.headers_mut();
345 headers.insert("x-conn-id", HeaderValue::from(conn_id as u64));
346 headers.insert("x-resp-id", HeaderValue::from(resp_id as u64));
347 Ok::<_, Infallible>(resp)
348 }
349 }))
350 }))
351 }
352
353 fn conn_id(resp: &Response) -> u64 {
354 resp.headers()
355 .get("x-conn-id")
356 .unwrap()
357 .to_str()
358 .unwrap()
359 .parse()
360 .unwrap()
361 }
362
363 #[tokio::test]
364 async fn pool_keeps_h2_connection_in_use_until_response_body_consumed() {
365 let connector = build_test_connector(
366 HttpPooledConnectorConfig {
367 max_concurrent_streams: 1,
368 max_total: 4,
369 ..Default::default()
370 },
371 tagging_mock_connector(),
372 );
373
374 let req = || create_test_request(Version::HTTP_2);
375
376 let est1 = connector.serve(req()).await.unwrap();
379 let resp1 = est1.conn.serve(req()).await.unwrap();
380 drop(est1);
381
382 let est2 = connector.serve(req()).await.unwrap();
384 let resp2 = est2.conn.serve(req()).await.unwrap();
385
386 assert_eq!(conn_id(&resp1), 0);
387 assert_eq!(
388 conn_id(&resp2),
389 1,
390 "second request must not reuse a connection whose response body is still in flight"
391 );
392 }
393
394 #[tokio::test]
395 async fn pool_keeps_h1_connection_in_use_until_response_body_consumed() {
396 let connector = build_test_connector(
397 HttpPooledConnectorConfig {
398 max_concurrent_streams: 1,
399 max_total: 4,
400 ..Default::default()
401 },
402 tagging_mock_connector(),
403 );
404
405 let req = || create_test_request(Version::HTTP_11);
406
407 let est1 = connector.serve(req()).await.unwrap();
408 let resp1 = est1.conn.serve(req()).await.unwrap();
409 drop(est1);
410
411 let est2 = connector.serve(req()).await.unwrap();
412 let resp2 = est2.conn.serve(req()).await.unwrap();
413
414 assert_eq!(conn_id(&resp1), 0);
415 assert_eq!(
416 conn_id(&resp2),
417 1,
418 "h1: second request must not reuse a connection whose response body is still in flight"
419 );
420 }
421
422 #[tokio::test(start_paused = true)]
427 async fn pool_does_not_reuse_h1_connection_after_server_close() {
428 let conns = Arc::new(AtomicUsize::new(0));
429 let inner =
430 HttpConnectorLayer::default().into_layer(MockConnectorService::new(move || {
431 let conn_id = conns.fetch_add(1, Ordering::Relaxed);
432 HttpServer::auto(Executor::default()).service(service_fn(
433 move |_req: Request| async move {
434 let mut resp = Response::new(Body::from("ok"));
435 let headers = resp.headers_mut();
436 headers.insert("x-conn-id", HeaderValue::from(conn_id as u64));
437 headers.insert("connection", HeaderValue::from_static("close"));
438 Ok::<_, Infallible>(resp)
439 },
440 ))
441 }));
442 let connector = build_test_connector(
443 HttpPooledConnectorConfig {
444 max_total: 4,
445 ..Default::default()
446 },
447 inner,
448 );
449
450 let req = || create_test_request(Version::HTTP_11);
451
452 let est1 = connector.serve(req()).await.unwrap();
455 let resp1 = est1.conn.serve(req()).await.unwrap();
456 drop(est1);
457 let id1 = conn_id(&resp1);
458 resp1.into_body().collect().await.unwrap();
459
460 sleep(Duration::from_millis(50)).await;
462
463 let est2 = connector.serve(req()).await.unwrap();
464 let resp2 = est2.conn.serve(req()).await.unwrap();
465
466 assert_eq!(id1, 0);
467 assert_ne!(
468 conn_id(&resp2),
469 id1,
470 "must not reuse an h1 connection the server closed"
471 );
472 }
473
474 #[tokio::test]
475 async fn pool_does_not_reuse_h1_connection_after_upgrade() {
476 let conns = Arc::new(AtomicUsize::new(0));
477 let inner =
478 HttpConnectorLayer::default().into_layer(MockConnectorService::new(move || {
479 let conn_id = conns.fetch_add(1, Ordering::Relaxed);
480 HttpServer::auto(Executor::default()).service(service_fn(
481 move |req: Request| async move {
482 let is_upgrade = req.uri().path().is_some_and(|path| path == "/upgrade");
483 if is_upgrade {
484 let on_upgrade = rama_http::io::upgrade::handle_upgrade(&req);
485 _ = tokio::spawn(async move {
486 _ = on_upgrade.await;
487 });
488 }
489
490 let mut resp = if is_upgrade {
491 Response::builder()
492 .status(StatusCode::SWITCHING_PROTOCOLS)
493 .header("connection", "upgrade")
494 .header("upgrade", "test")
495 .body(Body::empty())
496 .unwrap()
497 } else {
498 Response::new(Body::from("ok"))
499 };
500 resp.headers_mut()
501 .insert("x-conn-id", HeaderValue::from(conn_id as u64));
502 Ok::<_, Infallible>(resp)
503 },
504 ))
505 }));
506 let connector = build_test_connector(HttpPooledConnectorConfig::default(), inner);
507
508 let upgrade_request = Request::builder()
509 .uri("https://www.example.com/upgrade")
510 .version(Version::HTTP_11)
511 .header("connection", "upgrade")
512 .header("upgrade", "test")
513 .body(Body::empty())
514 .unwrap();
515 let established = connector.serve(upgrade_request).await.unwrap();
516 let response = established.conn.serve(established.input).await.unwrap();
517 let first_conn_id = conn_id(&response);
518 assert_eq!(
519 established
520 .conn
521 .extensions()
522 .get_ref::<ConnectionHealthWatcher>()
523 .expect("pooled h1 connection health watcher")
524 .health(),
525 ConnectionHealth::Broken,
526 "an h1 upgrade must be marked broken before returning its response"
527 );
528 let on_upgrade = rama_http::io::upgrade::handle_upgrade(&response);
529 let body = response.into_body();
530 drop(body);
531
532 let request = create_test_request(Version::HTTP_11);
533 let established = connector.serve(request).await.unwrap();
534 let response = established.conn.serve(established.input).await.unwrap();
535 assert_ne!(
536 conn_id(&response),
537 first_conn_id,
538 "an upgraded h1 connection must be evicted before the next request"
539 );
540 drop(on_upgrade.await.unwrap());
541 }
542
543 #[tokio::test(start_paused = true)]
548 async fn pool_does_not_reuse_h1_connection_after_body_dropped_early() {
549 let conns = Arc::new(AtomicUsize::new(0));
550 let inner =
551 HttpConnectorLayer::default().into_layer(MockConnectorService::new(move || {
552 let conn_id = conns.fetch_add(1, Ordering::Relaxed);
553 HttpServer::auto(Executor::default()).service(service_fn(
554 move |_req: Request| async move {
555 let mut resp = Response::new(Body::from(vec![0u8; kib(1024)]));
556 resp.headers_mut()
557 .insert("x-conn-id", HeaderValue::from(conn_id as u64));
558 Ok::<_, Infallible>(resp)
559 },
560 ))
561 }));
562 let connector = build_test_connector(
563 HttpPooledConnectorConfig {
564 max_total: 4,
565 ..Default::default()
566 },
567 inner,
568 );
569
570 let req = || create_test_request(Version::HTTP_11);
571
572 let est1 = connector.serve(req()).await.unwrap();
574 let resp1 = est1.conn.serve(req()).await.unwrap();
575 drop(est1);
576 let id1 = conn_id(&resp1);
577 drop(resp1);
578
579 sleep(Duration::from_millis(50)).await;
581
582 let est2 = connector.serve(req()).await.unwrap();
583 let resp2 = est2.conn.serve(req()).await.unwrap();
584
585 assert_eq!(id1, 0);
586 assert_ne!(
587 conn_id(&resp2),
588 id1,
589 "must not reuse an h1 connection whose response body was abandoned mid-stream"
590 );
591 }
592
593 #[tokio::test]
594 async fn pool_reuses_connection_after_body_consumed() {
595 let connector = build_test_connector(
596 HttpPooledConnectorConfig {
597 max_concurrent_streams: 1,
598 max_total: 4,
599 ..Default::default()
600 },
601 tagging_mock_connector(),
602 );
603
604 let req = || create_test_request(Version::HTTP_2);
605
606 let resp1 = connector
607 .serve(req())
608 .await
609 .unwrap()
610 .conn
611 .serve(req())
612 .await
613 .unwrap();
614 assert_eq!(conn_id(&resp1), 0);
615 resp1.into_body().collect().await.unwrap();
617
618 let resp2 = connector
619 .serve(req())
620 .await
621 .unwrap()
622 .conn
623 .serve(req())
624 .await
625 .unwrap();
626 assert_eq!(
627 conn_id(&resp2),
628 0,
629 "connection must be reused once its response body is consumed"
630 );
631 }
632
633 #[tokio::test]
634 async fn pool_multiplexes_on_h2() {
635 let connector = build_test_connector(
636 HttpPooledConnectorConfig::default(),
637 tagging_mock_connector(),
638 );
639
640 let req = || create_test_request(Version::HTTP_2);
641
642 let resp1 = connector
644 .serve(req())
645 .await
646 .unwrap()
647 .conn
648 .serve(req())
649 .await
650 .unwrap();
651 let resp2 = connector
652 .serve(req())
653 .await
654 .unwrap()
655 .conn
656 .serve(req())
657 .await
658 .unwrap();
659
660 assert_eq!(conn_id(&resp1), 0);
661 assert_eq!(
662 conn_id(&resp2),
663 0,
664 "h2: a second in-flight request multiplexes onto the same connection"
665 );
666 }
667
668 #[tokio::test]
669 async fn pool_does_not_multiplex_on_h1() {
670 let connector = build_test_connector(
671 HttpPooledConnectorConfig::default(),
672 tagging_mock_connector(),
673 );
674
675 let req = || create_test_request(Version::HTTP_11);
676
677 let resp1 = connector
678 .serve(req())
679 .await
680 .unwrap()
681 .conn
682 .serve(req())
683 .await
684 .unwrap();
685 let resp2 = connector
686 .serve(req())
687 .await
688 .unwrap()
689 .conn
690 .serve(req())
691 .await
692 .unwrap();
693
694 assert_eq!(conn_id(&resp1), 0);
695 assert_eq!(
696 conn_id(&resp2),
697 1,
698 "h1 does not multiplex: a second in-flight request needs a new connection"
699 );
700 }
701
702 #[tokio::test]
703 async fn pool_respects_max_concurrent_streams() {
704 let connector = build_test_connector(
705 HttpPooledConnectorConfig {
706 max_concurrent_streams: 2,
707 max_total: 4,
708 ..Default::default()
709 },
710 tagging_mock_connector(),
711 );
712
713 let req = || create_test_request(Version::HTTP_2);
714
715 let resp1 = connector
717 .serve(req())
718 .await
719 .unwrap()
720 .conn
721 .serve(req())
722 .await
723 .unwrap();
724 let resp2 = connector
725 .serve(req())
726 .await
727 .unwrap()
728 .conn
729 .serve(req())
730 .await
731 .unwrap();
732 let resp3 = connector
733 .serve(req())
734 .await
735 .unwrap()
736 .conn
737 .serve(req())
738 .await
739 .unwrap();
740
741 assert_eq!(conn_id(&resp1), 0);
742 assert_eq!(conn_id(&resp2), 0, "second request fits on connection 0");
743 assert_eq!(
744 conn_id(&resp3),
745 1,
746 "third request exceeds the per-connection limit, needs a new connection"
747 );
748 }
749
750 fn large_body_mock_connector() -> impl ConnectorService<
753 ConnectRequest,
754 Connection: Service<Request, Output = Response, Error = BoxError> + ExtensionsRef,
755 > {
756 let conns = Arc::new(AtomicUsize::new(0));
757 HttpConnectorLayer::default().into_layer(MockConnectorService::new(move || {
758 let conn_id = conns.fetch_add(1, Ordering::Relaxed);
759 HttpServer::auto(Executor::default()).service(service_fn(
760 move |_req: Request| async move {
761 let mut resp = Response::new(Body::from(vec![0u8; kib(1024)]));
762 resp.headers_mut()
763 .insert("x-conn-id", HeaderValue::from(conn_id as u64));
764 Ok::<_, Infallible>(resp)
765 },
766 ))
767 }))
768 }
769
770 #[tokio::test]
775 async fn pool_binds_connection_across_streaming_body() {
776 let connector = build_test_connector(
777 HttpPooledConnectorConfig {
778 max_concurrent_streams: 1,
779 max_total: 4,
780 ..Default::default()
781 },
782 large_body_mock_connector(),
783 );
784
785 let req = || create_test_request(Version::HTTP_2);
786
787 let resp1 = connector
790 .serve(req())
791 .await
792 .unwrap()
793 .conn
794 .serve(req())
795 .await
796 .unwrap();
797 let id1 = conn_id(&resp1);
798 let mut body1 = resp1.into_body();
799 assert!(
800 body1.frame().await.is_some(),
801 "streaming body should yield at least one frame"
802 );
803
804 let resp2 = connector
806 .serve(req())
807 .await
808 .unwrap()
809 .conn
810 .serve(req())
811 .await
812 .unwrap();
813 assert_eq!(id1, 0);
814 assert_ne!(
815 conn_id(&resp2),
816 id1,
817 "a connection still streaming its response body must not be reused"
818 );
819
820 while let Some(frame) = body1.frame().await {
823 frame.unwrap();
824 }
825
826 let resp3 = connector
829 .serve(req())
830 .await
831 .unwrap()
832 .conn
833 .serve(req())
834 .await
835 .unwrap();
836 assert_eq!(
837 conn_id(&resp3),
838 id1,
839 "a connection is reused once its streaming body reaches end-of-stream"
840 );
841 }
842}