1use std::{
18 sync::{
19 Arc,
20 atomic::{AtomicU64, Ordering},
21 },
22 time::{Duration, Instant},
23};
24
25use bytes::Bytes;
26use scion_quic::h3::client::{
27 H3DuplexStream, H3ResponseBody, Http3Client, RequestError as H3RequestError,
28};
29
30use crate::{
31 authority::Authority,
32 config::Config,
33 epoch::Epoch,
34 error::{Error, TimeoutPhase},
35 origin::Origin,
36 request::{IntoUrl, Request},
37 response::Response,
38 tunnel::{Tunnel, connect_request},
39};
40
41enum EpochSlot {
43 Init,
46 Active(Arc<Epoch>, u64),
50 Closed,
52}
53
54pub struct Client {
74 epoch: tokio::sync::RwLock<EpochSlot>,
76 reset_generation: AtomicU64,
82 config: Config,
84 #[cfg(test)]
87 epoch_factory: Option<EpochFactory>,
88}
89
90#[cfg(test)]
91type EpochFactory =
92 Box<dyn Fn() -> futures::future::BoxFuture<'static, Result<Epoch, Error>> + Send + Sync>;
93
94impl Client {
95 #[must_use]
98 pub fn new(config: Config) -> Self {
99 Client {
100 epoch: tokio::sync::RwLock::new(EpochSlot::Init),
101 reset_generation: AtomicU64::new(0),
102 config,
103 #[cfg(test)]
104 epoch_factory: None,
105 }
106 }
107
108 #[cfg(test)]
111 pub(crate) fn with_epoch_factory(config: Config, factory: EpochFactory) -> Self {
112 let mut client = Client::new(config);
113 client.epoch_factory = Some(factory);
114 client
115 }
116
117 pub async fn request(&self, request: Request) -> Result<Response, Error> {
132 let timeout = request
133 .request_timeout()
134 .unwrap_or(self.config.request_timeout);
135 let deadline = deadline_after(timeout);
136 let (response, connection) = tokio::time::timeout_at(deadline, self.request_head(&request))
137 .await
138 .map_err(|_| {
139 Error::Timeout {
140 phase: TimeoutPhase::Request,
141 timeout,
142 }
143 })??;
144 Ok(Response::new(response, deadline, timeout, connection))
145 }
146
147 pub async fn get(&self, url: impl IntoUrl) -> Result<Response, Error> {
150 let request = Request::get(url).build()?;
151 self.request(request).await
152 }
153
154 pub async fn post(&self, url: impl IntoUrl, body: impl Into<Bytes>) -> Result<Response, Error> {
158 let request = Request::post(url).body(body).build()?;
159 self.request(request).await
160 }
161
162 pub async fn connect(&self, authority: &Authority) -> Result<Tunnel, Error> {
169 let timeout = self.config.request_timeout;
170 tokio::time::timeout_at(deadline_after(timeout), self.connect_tunnel(authority))
171 .await
172 .map_err(|_| {
173 Error::Timeout {
174 phase: TimeoutPhase::Request,
175 timeout,
176 }
177 })?
178 }
179
180 pub async fn warm_up(&self, url: impl IntoUrl) -> Result<(), Error> {
185 let request = Request::get(url).build()?;
186 let origin = Origin::from_request(&request)?;
187 let epoch = self.current_epoch().await?;
188 let origin_client = epoch.origin_client(origin, Instant::now(), &self.config)?;
189 origin_client.connection().await?;
190 Ok(())
191 }
192
193 pub fn reset(&self) {
201 self.reset_generation.fetch_add(1, Ordering::Release);
202 }
203
204 pub async fn close(&self) {
208 let previous = {
209 let mut slot = self.epoch.write().await;
210 std::mem::replace(&mut *slot, EpochSlot::Closed)
211 };
212 if let EpochSlot::Active(epoch, _) = previous {
213 epoch.shutdown().await;
214 }
215 }
216
217 async fn request_head(
222 &self,
223 request: &Request,
224 ) -> Result<(http::Response<H3ResponseBody>, Arc<Http3Client>), Error> {
225 let origin = Origin::from_request(request)?;
226 self.with_connection(origin, |connection| {
227 async move { Ok(connection.request(request.to_http()?).await?) }
228 })
229 .await
230 }
231
232 async fn connect_tunnel(&self, authority: &Authority) -> Result<Tunnel, Error> {
235 let origin = Origin::from_authority(authority);
236 let (host, port) = (origin.host.clone(), origin.port);
237 let ((response, writer), connection) = self
238 .with_connection(origin, |connection| {
239 async move {
240 Ok(connection
241 .request_with_writer(connect_request(authority))
242 .await?)
243 }
244 })
245 .await?;
246 let response = response
247 .await
248 .map_err(|e| Error::from_h3_request_error(&host, port, e))?;
249 if !response.status().is_success() {
250 return Err(Error::TunnelRefused {
251 status: response.status(),
252 });
253 }
254 let stream = H3DuplexStream::new(writer, response.into_body());
255 Ok(Tunnel::new(stream, connection))
256 }
257
258 async fn with_connection<T, F, Fut>(
264 &self,
265 origin: Origin,
266 send: F,
267 ) -> Result<(T, Arc<Http3Client>), Error>
268 where
269 F: Fn(Arc<Http3Client>) -> Fut,
270 Fut: Future<Output = Result<T, SendError>>,
271 {
272 let (host, port) = (origin.host.clone(), origin.port);
273 let epoch = self.current_epoch().await?;
274 let origin_client = epoch.origin_client(origin, Instant::now(), &self.config)?;
275
276 let (connection, generation) = origin_client.connection().await?;
277 match send(connection.clone()).await {
278 Ok(output) => Ok((output, connection)),
279 Err(SendError::Transport(H3RequestError::Establish(_))) => {
280 let (connection, _) = origin_client.reconnect(generation).await?;
281 let output = send(connection.clone())
282 .await
283 .map_err(|e| e.into_error(&host, port))?;
284 Ok((output, connection))
285 }
286 Err(e) => Err(e.into_error(&host, port)),
287 }
288 }
289
290 async fn current_epoch(&self) -> Result<Arc<Epoch>, Error> {
298 {
299 let slot = self.epoch.read().await;
300 match &*slot {
301 EpochSlot::Closed => return Err(Error::Closed),
302 EpochSlot::Active(epoch, built)
303 if *built == self.reset_generation.load(Ordering::Acquire) =>
304 {
305 return Ok(epoch.clone());
306 }
307 _ => {}
308 }
309 }
310
311 let mut slot = self.epoch.write().await;
312 match &*slot {
313 EpochSlot::Closed => Err(Error::Closed),
314 EpochSlot::Active(epoch, built)
315 if *built == self.reset_generation.load(Ordering::Acquire) =>
316 {
317 Ok(epoch.clone())
318 }
319 _ => {
320 let generation = self.reset_generation.load(Ordering::Acquire);
325 let fresh = Arc::new(self.build_epoch().await?);
326 let previous =
327 std::mem::replace(&mut *slot, EpochSlot::Active(fresh.clone(), generation));
328 if let EpochSlot::Active(previous, _) = previous {
329 tracing::debug!("Replacing SCION connectivity epoch");
332 tokio::spawn(previous.shutdown());
333 }
334 Ok(fresh)
335 }
336 }
337 }
338
339 async fn build_epoch(&self) -> Result<Epoch, Error> {
340 #[cfg(test)]
341 if let Some(factory) = &self.epoch_factory {
342 return factory().await;
343 }
344 Epoch::build(&self.config).await
345 }
346}
347
348enum SendError {
350 Transport(H3RequestError),
351 Request(Error),
352}
353
354impl SendError {
355 fn into_error(self, host: &str, port: u16) -> Error {
356 match self {
357 SendError::Transport(e) => Error::from_h3_request_error(host, port, e),
358 SendError::Request(e) => e,
359 }
360 }
361}
362
363impl From<H3RequestError> for SendError {
364 fn from(e: H3RequestError) -> Self {
365 SendError::Transport(e)
366 }
367}
368
369impl From<Error> for SendError {
370 fn from(e: Error) -> Self {
371 SendError::Request(e)
372 }
373}
374
375fn deadline_after(timeout: Duration) -> tokio::time::Instant {
378 let now = tokio::time::Instant::now();
379 now.checked_add(timeout)
380 .unwrap_or_else(|| now + Duration::from_hours(24 * 365 * 30))
381}
382
383#[cfg(test)]
384mod tests {
385 use std::{
386 sync::atomic::{AtomicUsize, Ordering::SeqCst},
387 time::Duration,
388 };
389
390 use scion_quic::quic::config::QuicConfig;
391 use test_log::test;
392 use tokio::io::{AsyncReadExt, AsyncWriteExt};
393
394 use super::*;
395 use crate::test_support::{
396 FailingResolver, SERVER_PORT, StaticResolver, TestServerHarness, TunnelEchoService,
397 server_scion_ip, test_config, test_router,
398 };
399
400 fn harness_client(
403 harness: Arc<TestServerHarness>,
404 resolver: Arc<dyn scion_stack::resolver::ScionDnsResolver>,
405 config: Config,
406 ) -> (Client, Arc<AtomicUsize>) {
407 let builds = Arc::new(AtomicUsize::new(0));
408 let factory_builds = builds.clone();
409 let factory_config = config.clone();
410 let client = Client::with_epoch_factory(
411 config,
412 Box::new(move || {
413 factory_builds.fetch_add(1, SeqCst);
414 let harness = harness.clone();
415 let resolver = resolver.clone();
416 let config = factory_config.clone();
417 Box::pin(async move { Ok(Epoch::new(harness, resolver, &config)) })
418 }),
419 );
420 (client, builds)
421 }
422
423 fn url(host: &str, path: &str) -> String {
424 format!("https://{host}:{SERVER_PORT}{path}")
425 }
426
427 fn authority(host: &str) -> Authority {
428 Authority::new(host, SERVER_PORT).unwrap()
429 }
430
431 fn tunnel_client(config: Config) -> (Client, Arc<TestServerHarness>, TunnelEchoService) {
432 let service = TunnelEchoService::default();
433 let harness = TestServerHarness::with_tunnel_service(service.clone());
434 let resolver = StaticResolver::new(vec![server_scion_ip()]);
435 let (client, _) = harness_client(harness.clone(), resolver, config);
436 (client, harness, service)
437 }
438
439 async fn echo_round_trip(tunnel: &mut Tunnel, payload: &[u8]) {
440 tunnel.write_all(payload).await.unwrap();
441 tunnel.flush().await.unwrap();
442 let mut echoed = vec![0u8; payload.len()];
443 tunnel.read_exact(&mut echoed).await.unwrap();
444 assert_eq!(echoed, payload);
445 }
446
447 #[test(tokio::test)]
448 #[ntest::timeout(10_000)]
449 async fn connect_tunnels_bytes_both_ways() {
450 let (client, _, service) = tunnel_client(test_config());
451
452 let mut tunnel = client.connect(&authority("localhost")).await.unwrap();
453 for round in 0..3 {
454 echo_round_trip(&mut tunnel, format!("chunk-{round}").as_bytes()).await;
455 }
456 assert_eq!(
457 service.requests(),
458 vec![(
459 http::Method::CONNECT,
460 Some(authority("localhost").to_string())
461 )]
462 );
463
464 tunnel.shutdown().await.unwrap();
465 let mut buf = [0u8; 16];
466 assert_eq!(tunnel.read(&mut buf).await.unwrap(), 0);
467 }
468
469 #[test(tokio::test)]
470 #[ntest::timeout(10_000)]
471 async fn connect_maps_non_2xx_to_tunnel_refused() {
472 let (client, _, service) = tunnel_client(test_config());
473
474 service.set_status(http::StatusCode::NOT_FOUND);
475 let err = client.connect(&authority("localhost")).await.err().unwrap();
476 assert!(
477 matches!(
478 err,
479 Error::TunnelRefused {
480 status: http::StatusCode::NOT_FOUND
481 }
482 ),
483 "{err}"
484 );
485 assert!(!err.is_retryable());
486
487 service.set_status(http::StatusCode::BAD_GATEWAY);
488 let err = client.connect(&authority("localhost")).await.err().unwrap();
489 assert!(
490 matches!(
491 err,
492 Error::TunnelRefused {
493 status: http::StatusCode::BAD_GATEWAY
494 }
495 ),
496 "{err}"
497 );
498 assert!(err.is_retryable());
499 }
500
501 #[test(tokio::test)]
502 #[ntest::timeout(10_000)]
503 async fn connect_shares_the_pooled_connection() {
504 let (client, harness, _) = tunnel_client(test_config());
505
506 let mut first = client.connect(&authority("localhost")).await.unwrap();
507 let mut second = client.connect(&authority("localhost")).await.unwrap();
508 echo_round_trip(&mut first, b"first").await;
509 echo_round_trip(&mut second, b"second").await;
510 assert_eq!(harness.binds.load(SeqCst), 1);
511 }
512
513 #[test(tokio::test)]
514 #[ntest::timeout(10_000)]
515 async fn connect_reuses_the_establish_retry() {
516 let (client, harness, _) = tunnel_client(test_config());
517
518 let tunnel = client.connect(&authority("localhost")).await.unwrap();
519 assert_eq!(harness.binds.load(SeqCst), 1);
520 drop(tunnel);
521
522 harness.break_sockets();
523 tokio::time::sleep(Duration::from_millis(200)).await;
524
525 let mut tunnel = client.connect(&authority("localhost")).await.unwrap();
526 echo_round_trip(&mut tunnel, b"after reconnect").await;
527 assert_eq!(harness.binds.load(SeqCst), 2);
528 }
529
530 #[test(tokio::test)]
531 #[ntest::timeout(10_000)]
532 async fn tunnel_survives_origin_eviction() {
533 let (client, ..) = tunnel_client(test_config().with_max_origins(1));
534
535 let mut tunnel = client.connect(&authority("a.local")).await.unwrap();
536 echo_round_trip(&mut tunnel, b"before eviction").await;
537
538 let mut other = client.connect(&authority("b.local")).await.unwrap();
539 echo_round_trip(&mut other, b"other origin").await;
540
541 echo_round_trip(&mut tunnel, b"after eviction").await;
542 }
543
544 #[test(tokio::test)]
545 #[ntest::timeout(10_000)]
546 async fn close_faults_open_tunnel() {
547 let (client, ..) = tunnel_client(test_config());
548 let client = Arc::new(client);
549
550 let mut tunnel = client.connect(&authority("localhost")).await.unwrap();
551 let pending = tokio::spawn(async move {
552 let mut buf = [0u8; 16];
553 tunnel.read(&mut buf).await
554 });
555 tokio::time::sleep(Duration::from_millis(100)).await;
556 client.close().await;
557
558 let start = tokio::time::Instant::now();
559 assert!(pending.await.unwrap().is_err());
560 assert!(start.elapsed() < Duration::from_millis(300));
561 }
562
563 #[test(tokio::test)]
564 #[ntest::timeout(10_000)]
565 async fn get_resolves_via_dns_path() {
566 let (router, hits) = test_router();
567 let harness = TestServerHarness::new(router);
568 let resolver = StaticResolver::new(vec![server_scion_ip()]);
569 let (client, _) = harness_client(harness, resolver, test_config());
570
571 let response = client.get(url("localhost", "/hello")).await.unwrap();
572 assert!(response.is_success());
573 let (body, _) = response.text(Some(1024)).await.unwrap();
574 assert_eq!(body, "world");
575 assert_eq!(hits.load(SeqCst), 1);
576 }
577
578 #[test(tokio::test)]
579 #[ntest::timeout(10_000)]
580 async fn duration_max_timeout_does_not_panic() {
581 let (router, _) = test_router();
582 let harness = TestServerHarness::new(router);
583 let resolver = StaticResolver::new(vec![server_scion_ip()]);
584 let (client, _) = harness_client(harness, resolver, test_config());
585
586 let request = Request::get(url("localhost", "/hello"))
587 .request_timeout(Duration::MAX)
588 .build()
589 .unwrap();
590 let response = client.request(request).await.unwrap();
591 assert!(response.is_success());
592 }
593
594 #[test(tokio::test)]
595 #[ntest::timeout(10_000)]
596 async fn resolution_failure_surfaces_as_retryable_resolution_error() {
597 let (router, _) = test_router();
598 let harness = TestServerHarness::new(router);
599 let (client, _) = harness_client(harness, Arc::new(FailingResolver), test_config());
600
601 let err = client.get(url("localhost", "/hello")).await.err().unwrap();
602 assert!(matches!(err, Error::Resolution { .. }), "{err}");
603 assert!(err.is_retryable());
604 }
605
606 #[test(tokio::test)]
607 #[ntest::timeout(10_000)]
608 async fn establish_failure_reconnects_once_and_retries() {
609 let (router, hits) = test_router();
610 let harness = TestServerHarness::new(router);
611 let resolver = StaticResolver::new(vec![server_scion_ip()]);
612 let (client, _) = harness_client(harness.clone(), resolver, test_config());
613
614 let response = client.get(url("localhost", "/hello")).await.unwrap();
615 assert!(response.is_success());
616 assert_eq!(harness.binds.load(SeqCst), 1);
617
618 harness.break_sockets();
623 tokio::time::sleep(Duration::from_millis(200)).await;
626
627 let response = client.get(url("localhost", "/hello")).await.unwrap();
628 assert!(response.is_success());
629 assert_eq!(harness.binds.load(SeqCst), 2);
630 assert_eq!(hits.load(SeqCst), 2);
631 }
632
633 #[test(tokio::test)]
634 #[ntest::timeout(10_000)]
635 async fn stream_level_errors_are_not_retried() {
636 let (router, _) = test_router();
637 let mut server_quic = QuicConfig::builder().verify_peer(false).build();
640 server_quic.initial_max_streams_bidi = 0;
641 let harness = TestServerHarness::with_server_quic(router, server_quic);
642 let resolver = StaticResolver::new(vec![server_scion_ip()]);
643 let (client, _) = harness_client(harness.clone(), resolver, test_config());
644
645 let err = client.get(url("localhost", "/hello")).await.err().unwrap();
646 assert!(matches!(err, Error::ConnectionLimit), "{err}");
647 assert_eq!(harness.binds.load(SeqCst), 1);
650 }
651
652 #[test(tokio::test)]
653 #[ntest::timeout(10_000)]
654 async fn reset_is_lazy_and_rebuild_is_single_flight() {
655 let (router, _) = test_router();
656 let harness = TestServerHarness::new(router);
657 let resolver = StaticResolver::new(vec![server_scion_ip()]);
658 let (client, builds) = harness_client(harness, resolver, test_config());
659 let client = Arc::new(client);
660
661 client.get(url("localhost", "/hello")).await.unwrap();
662 assert_eq!(builds.load(SeqCst), 1);
663
664 client.reset();
666 assert_eq!(builds.load(SeqCst), 1);
667
668 let tasks: Vec<_> = (0..3)
671 .map(|_| {
672 let client = client.clone();
673 tokio::spawn(async move { client.get(url("localhost", "/hello")).await })
674 })
675 .collect();
676 for task in tasks {
677 task.await.unwrap().unwrap();
678 }
679 assert_eq!(builds.load(SeqCst), 2);
680 }
681
682 #[test(tokio::test)]
683 #[ntest::timeout(10_000)]
684 async fn reset_during_rebuild_triggers_another_rebuild() {
685 let (router, _) = test_router();
686 let harness = TestServerHarness::new(router);
687 let resolver = StaticResolver::new(vec![server_scion_ip()]);
688 let builds = Arc::new(AtomicUsize::new(0));
689 let gate = Arc::new(tokio::sync::Semaphore::new(1));
692 let factory_builds = builds.clone();
693 let factory_gate = gate.clone();
694 let config = test_config();
695 let factory_config = config.clone();
696 let client = Arc::new(Client::with_epoch_factory(
697 config,
698 Box::new(move || {
699 let builds = factory_builds.clone();
700 let gate = factory_gate.clone();
701 let harness = harness.clone();
702 let resolver = resolver.clone();
703 let config = factory_config.clone();
704 Box::pin(async move {
705 gate.acquire().await.expect("gate closed").forget();
706 builds.fetch_add(1, SeqCst);
707 Ok(Epoch::new(harness, resolver, &config))
708 })
709 }),
710 ));
711
712 client.get(url("localhost", "/hello")).await.unwrap();
713 assert_eq!(builds.load(SeqCst), 1);
714
715 client.reset();
717 let pending = {
718 let client = client.clone();
719 tokio::spawn(async move { client.get(url("localhost", "/hello")).await })
720 };
721 tokio::time::sleep(Duration::from_millis(100)).await;
722 assert_eq!(builds.load(SeqCst), 1);
723
724 client.reset();
727 gate.add_permits(2);
728 pending.await.unwrap().unwrap();
729 assert_eq!(builds.load(SeqCst), 2);
730
731 client.get(url("localhost", "/hello")).await.unwrap();
735 assert_eq!(builds.load(SeqCst), 3);
736 }
737
738 #[test(tokio::test)]
739 #[ntest::timeout(10_000)]
740 async fn failed_rebuild_keeps_stale_and_retries_next_request() {
741 let (router, _) = test_router();
742 let harness = TestServerHarness::new(router);
743 let resolver = StaticResolver::new(vec![server_scion_ip()]);
744 let builds = Arc::new(AtomicUsize::new(0));
745 let factory_builds = builds.clone();
746 let config = test_config();
747 let factory_config = config.clone();
748 let client = Client::with_epoch_factory(
749 config,
750 Box::new(move || {
751 let build = factory_builds.fetch_add(1, SeqCst) + 1;
752 let harness = harness.clone();
753 let resolver = resolver.clone();
754 let config = factory_config.clone();
755 Box::pin(async move {
756 if build == 2 {
757 return Err(Error::StackBuild {
758 retryable: true,
759 source: "flaky discovery".into(),
760 });
761 }
762 Ok(Epoch::new(harness, resolver, &config))
763 })
764 }),
765 );
766
767 client.get(url("localhost", "/hello")).await.unwrap();
768 client.reset();
769
770 let err = client.get(url("localhost", "/hello")).await.err().unwrap();
772 assert!(matches!(err, Error::StackBuild { .. }), "{err}");
773 assert!(err.is_retryable());
774
775 client.get(url("localhost", "/hello")).await.unwrap();
777 assert_eq!(builds.load(SeqCst), 3);
778 }
779
780 #[test(tokio::test)]
781 #[ntest::timeout(10_000)]
782 async fn epochs_do_not_share_stacks_across_reset() {
783 let (router_one, _) = test_router();
785 let (router_two, _) = test_router();
786 let first = TestServerHarness::new(router_one);
787 let second = TestServerHarness::new(router_two);
788 let resolver = StaticResolver::new(vec![server_scion_ip()]);
789 let builds = Arc::new(AtomicUsize::new(0));
790 let factory_builds = builds.clone();
791 let config = test_config();
792 let factory_config = config.clone();
793 let factory_first = first.clone();
794 let factory_second = second.clone();
795 let client = Client::with_epoch_factory(
796 config,
797 Box::new(move || {
798 let build = factory_builds.fetch_add(1, SeqCst) + 1;
799 let harness = if build == 1 {
800 factory_first.clone()
801 } else {
802 factory_second.clone()
803 };
804 let resolver = resolver.clone();
805 let config = factory_config.clone();
806 Box::pin(async move { Ok(Epoch::new(harness, resolver, &config)) })
807 }),
808 );
809
810 client.get(url("localhost", "/hello")).await.unwrap();
811 assert_eq!(
812 (first.binds.load(SeqCst), second.binds.load(SeqCst)),
813 (1, 0)
814 );
815
816 client.reset();
817 client.get(url("localhost", "/hello")).await.unwrap();
818 assert_eq!(
819 (first.binds.load(SeqCst), second.binds.load(SeqCst)),
820 (1, 1)
821 );
822 }
823
824 #[test(tokio::test)]
825 #[ntest::timeout(10_000)]
826 async fn stalled_establishment_does_not_block_other_origins() {
827 let (router, _) = test_router();
830 let harness = TestServerHarness::new(router);
831 harness.stall_first_bind();
832 let resolver = StaticResolver::new(vec![server_scion_ip()]);
833 let (client, _) = harness_client(harness.clone(), resolver, test_config());
834 let client = Arc::new(client);
835
836 let stalled = {
837 let client = client.clone();
838 tokio::spawn(async move { client.get(url("a.local", "/hello")).await })
839 };
840 tokio::time::sleep(Duration::from_millis(100)).await;
841 assert!(!stalled.is_finished());
842
843 let response = client.get(url("b.local", "/hello")).await.unwrap();
846 assert!(response.is_success());
847 assert!(!stalled.is_finished());
848
849 harness.release_stalled();
850 stalled.await.unwrap().unwrap();
851 }
852
853 #[test(tokio::test)]
854 #[ntest::timeout(10_000)]
855 async fn evicted_origin_completes_in_flight_request() {
856 let (router, _) = test_router();
859 let harness = TestServerHarness::new(router);
860 let resolver = StaticResolver::new(vec![server_scion_ip()]);
861 let (client, _) = harness_client(harness, resolver, test_config().with_max_origins(1));
862 let client = Arc::new(client);
863
864 let slow = {
865 let client = client.clone();
866 tokio::spawn(async move {
867 let response = client.get(url("a.local", "/slow")).await?;
868 response.text(Some(1024)).await
869 })
870 };
871 tokio::time::sleep(Duration::from_millis(150)).await;
872
873 client.get(url("b.local", "/hello")).await.unwrap();
876
877 let (body, _) = slow.await.unwrap().unwrap();
878 assert_eq!(body, "slow");
879 }
880
881 #[test(tokio::test)]
882 #[ntest::timeout(10_000)]
883 async fn close_rejects_new_requests_and_faults_in_flight() {
884 let (router, _) = test_router();
885 let harness = TestServerHarness::new(router);
886 let resolver = StaticResolver::new(vec![server_scion_ip()]);
887 let (client, _) = harness_client(harness, resolver, test_config());
888 let client = Arc::new(client);
889
890 client.get(url("localhost", "/hello")).await.unwrap();
891
892 let in_flight = {
895 let client = client.clone();
896 tokio::spawn(async move {
897 let response = client.get(url("localhost", "/slow")).await?;
898 response.text(Some(1024)).await
899 })
900 };
901 tokio::time::sleep(Duration::from_millis(100)).await;
902 client.close().await;
903
904 let start = tokio::time::Instant::now();
905 let result = in_flight.await.unwrap();
906 assert!(result.is_err());
907 assert!(start.elapsed() < Duration::from_millis(300));
908
909 let err = client.get(url("localhost", "/hello")).await.err().unwrap();
911 assert!(matches!(err, Error::Closed), "{err}");
912 client.close().await;
913 }
914
915 #[test(tokio::test)]
916 #[ntest::timeout(10_000)]
917 async fn per_request_timeout_overrides_the_default() {
918 let (router, _) = test_router();
919 let harness = TestServerHarness::new(router);
920 let resolver = StaticResolver::new(vec![server_scion_ip()]);
921 let (client, _) = harness_client(harness, resolver, test_config());
922
923 let request = Request::get(url("localhost", "/slow"))
924 .request_timeout(Duration::from_millis(100))
925 .build()
926 .unwrap();
927 let err = client.request(request).await.err().unwrap();
928 assert!(
929 matches!(
930 err,
931 Error::Timeout {
932 phase: TimeoutPhase::Request,
933 ..
934 }
935 ),
936 "{err}"
937 );
938 }
939
940 #[test(tokio::test)]
941 #[ntest::timeout(10_000)]
942 async fn body_collection_respects_max_size() {
943 let (router, _) = test_router();
944 let harness = TestServerHarness::new(router);
945 let resolver = StaticResolver::new(vec![server_scion_ip()]);
946 let (client, _) = harness_client(harness, resolver, test_config());
947
948 let response = client.get(url("localhost", "/hello")).await.unwrap();
949 let err = response.bytes(Some(2)).await.err().unwrap();
950 assert!(matches!(err, Error::BodyTooLarge { limit: 2 }), "{err}");
951 }
952
953 #[test(tokio::test)]
954 #[ntest::timeout(10_000)]
955 async fn warm_up_establishes_without_a_request() {
956 let (router, hits) = test_router();
957 let harness = TestServerHarness::new(router);
958 let resolver = StaticResolver::new(vec![server_scion_ip()]);
959 let (client, builds) = harness_client(harness.clone(), resolver, test_config());
960
961 client.warm_up(url("localhost", "/")).await.unwrap();
962 assert_eq!(builds.load(SeqCst), 1);
963 assert_eq!(harness.binds.load(SeqCst), 1);
964 assert_eq!(hits.load(SeqCst), 0);
965
966 client.get(url("localhost", "/hello")).await.unwrap();
968 assert_eq!(harness.binds.load(SeqCst), 1);
969 assert_eq!(hits.load(SeqCst), 1);
970 }
971}