1use std::{env::VarError, fmt, sync::Arc};
2
3use rama_core::{
4 Layer, Service,
5 error::{BoxError, BoxErrorExt as _, ErrorContext as _, ErrorExt as _},
6 error_sink::ErrorSink,
7 extensions::ExtensionsRef,
8};
9use rama_utils::macros::generate_set_and_with;
10use rama_utils::str::trim_non_empty;
11use tokio::sync::OnceCell;
12
13use crate::{
14 Protocol,
15 address::{Authority, HostWithOptPort, HostWithPort, ProxyAddress},
16 input_ext::{AuthorityInputExt, ProtocolInputExt, UriInputExt},
17 user::ProxyCredential,
18};
19
20use super::{
21 ProxyRoute,
22 bypass::{BypassRule, BypassRuleDialect},
23 load::{CachedLoadError, LoadErrorPolicy},
24 system::{is_already_routed, request_protocol},
25};
26const ALL_PROXY_ENV: &[&str] = &["all_proxy", "ALL_PROXY"];
27const NO_PROXY_ENV: &[&str] = &["no_proxy", "NO_PROXY"];
28const MAX_CACHED_PROXY_SCHEMES: u64 = 64;
29
30type EnvironmentReader = dyn Fn(&str) -> Result<Option<String>, BoxError> + Send + Sync + 'static;
31
32pub(super) fn proxy_address_from_env(key: &str) -> Result<Option<ProxyAddress>, BoxError> {
33 let value = read_proxy_environment_variable(key)?;
34 parse_proxy_address_env_value(value.as_deref())
35}
36
37fn read_proxy_environment_variable(key: &str) -> Result<Option<String>, BoxError> {
38 if key.is_empty() || key.bytes().any(|byte| byte == b'\0' || byte == b'=') {
39 return Err(
40 BoxError::from_static_str("invalid environment variable name")
41 .context_str_field("environment_variable", key),
42 );
43 }
44 match std::env::var(key) {
45 Ok(value) => Ok(Some(value)),
46 Err(VarError::NotPresent) => Ok(None),
47 Err(error @ VarError::NotUnicode(_)) => Err(error
48 .context("read proxy environment variable")
49 .context_str_field("environment_variable", key)),
50 }
51}
52
53fn parse_proxy_address_env_value(value: Option<&str>) -> Result<Option<ProxyAddress>, BoxError> {
54 value
55 .and_then(trim_non_empty)
56 .map(|value| parse_proxy_environment_address(value).context("parse std env proxy info"))
57 .transpose()
58}
59
60fn parse_proxy_environment_address(value: &str) -> Result<ProxyAddress, BoxError> {
61 if let Ok(mut proxy) = value.parse::<ProxyAddress>() {
62 if proxy.protocol.is_none() {
63 proxy.protocol = Some(Protocol::HTTP);
64 }
65 return Ok(proxy);
66 }
67
68 let Authority {
69 user_info,
70 address: HostWithOptPort { host, port },
71 } = Authority::try_from(value)?;
72 let port = port.as_u16().unwrap_or(Protocol::HTTP_PROXY_DEFAULT_PORT);
73 Ok(ProxyAddress {
74 protocol: Some(Protocol::HTTP),
75 address: HostWithPort::new(host, port),
76 credential: user_info
77 .and_then(|user_info| user_info.to_basic().ok())
78 .map(ProxyCredential::Basic),
79 })
80}
81
82fn env_names(names: impl IntoIterator<Item = impl Into<Box<str>>>) -> Arc<[Box<str>]> {
83 names.into_iter().map(Into::into).collect()
84}
85
86fn default_env_names(names: &'static [&'static str]) -> Arc<[Box<str>]> {
87 env_names(names.iter().copied())
88}
89
90fn first_non_empty_value<'a>(
91 names: &'a [Box<str>],
92 reader: &EnvironmentReader,
93) -> Result<Option<(&'a str, String)>, BoxError> {
94 for name in names {
95 let Some(value) = reader(name)? else {
96 continue;
97 };
98 if trim_non_empty(&value).is_some() {
99 return Ok(Some((name, value)));
100 }
101 }
102 Ok(None)
103}
104
105#[derive(Clone)]
106struct LazyProxyAddress {
107 names: Arc<[Box<str>]>,
108 reader: Arc<EnvironmentReader>,
109 cached: Arc<OnceCell<Result<Option<ProxyAddress>, CachedLoadError>>>,
110}
111
112impl fmt::Debug for LazyProxyAddress {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 f.debug_struct("LazyProxyAddress")
115 .field("names", &self.names)
116 .field("cached", &self.cached.get())
117 .finish_non_exhaustive()
118 }
119}
120
121impl LazyProxyAddress {
122 fn new(names: &'static [&'static str], reader: Arc<EnvironmentReader>) -> Self {
123 Self::with_names(default_env_names(names), reader)
124 }
125
126 fn with_names(names: Arc<[Box<str>]>, reader: Arc<EnvironmentReader>) -> Self {
127 Self {
128 names,
129 reader,
130 cached: Arc::new(OnceCell::new()),
131 }
132 }
133
134 fn set_names(&mut self, names: impl IntoIterator<Item = impl Into<Box<str>>>) {
135 self.names = env_names(names);
136 self.cached = Arc::new(OnceCell::new());
137 }
138
139 fn reset(&mut self) {
140 self.cached = Arc::new(OnceCell::new());
141 }
142
143 async fn load(&self, policy: &LoadErrorPolicy) -> Result<Option<ProxyAddress>, BoxError> {
144 match self
145 .cached
146 .get_or_init(|| async {
147 match self.load_uncached() {
148 Ok(address) => Ok(address),
149 Err(error) => policy.handle_cached(error, None),
150 }
151 })
152 .await
153 {
154 Ok(address) => Ok(address.clone()),
155 Err(error) => Err(Box::new(error.clone())),
156 }
157 }
158
159 fn load_uncached(&self) -> Result<Option<ProxyAddress>, BoxError> {
160 let Some((name, value)) = first_non_empty_value(&self.names, self.reader.as_ref())? else {
161 return Ok(None);
162 };
163 parse_proxy_environment_address(value.trim())
164 .map(Some)
165 .context("parse proxy environment variable")
166 .context_str_field("environment_variable", name)
167 }
168}
169
170#[derive(Clone)]
171struct LazySchemeProxyAddresses {
172 reader: Arc<EnvironmentReader>,
173 overrides: Arc<ahash::HashMap<Protocol, Arc<[Box<str>]>>>,
174 cached: moka::sync::Cache<Protocol, LazyProxyAddress>,
175}
176
177impl fmt::Debug for LazySchemeProxyAddresses {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 f.debug_struct("LazySchemeProxyAddresses")
180 .field("overrides", &self.overrides)
181 .field("cached_protocol_count", &self.cached.entry_count())
182 .finish_non_exhaustive()
183 }
184}
185
186impl LazySchemeProxyAddresses {
187 fn new(reader: Arc<EnvironmentReader>) -> Self {
188 Self {
189 reader,
190 overrides: Arc::new(ahash::HashMap::default()),
191 cached: new_proxy_scheme_cache(),
192 }
193 }
194
195 fn set_names(
196 &mut self,
197 protocol: Protocol,
198 names: impl IntoIterator<Item = impl Into<Box<str>>>,
199 ) {
200 let mut overrides = self.overrides.as_ref().clone();
201 overrides.insert(protocol, env_names(names));
202 self.overrides = Arc::new(overrides);
203 self.reset();
204 }
205
206 fn reset(&mut self) {
207 self.cached = new_proxy_scheme_cache();
208 }
209
210 async fn load(
211 &self,
212 protocol: &Protocol,
213 policy: &LoadErrorPolicy,
214 ) -> Result<Option<ProxyAddress>, BoxError> {
215 let loader = self.cached.get_with(protocol.clone(), || {
216 let names = self
217 .overrides
218 .get(protocol)
219 .cloned()
220 .unwrap_or_else(|| default_scheme_env_names(protocol));
221 LazyProxyAddress::with_names(names, self.reader.clone())
222 });
223 loader.load(policy).await
224 }
225}
226
227fn new_proxy_scheme_cache() -> moka::sync::Cache<Protocol, LazyProxyAddress> {
228 moka::sync::Cache::builder()
229 .max_capacity(MAX_CACHED_PROXY_SCHEMES)
230 .build()
231}
232
233fn default_scheme_env_names(protocol: &Protocol) -> Arc<[Box<str>]> {
234 let scheme = protocol.as_str();
235 if *protocol == Protocol::HTTP {
236 return env_names([format!("{scheme}_proxy")]);
237 }
238 env_names([
239 format!("{scheme}_proxy"),
240 format!("{}_PROXY", scheme.to_ascii_uppercase()),
241 ])
242}
243
244#[derive(Clone)]
262pub struct ProxyEnvLayer {
263 schemes: LazySchemeProxyAddresses,
264 all: LazyProxyAddress,
265 load_error_policy: LoadErrorPolicy,
266 overwrite: bool,
267}
268
269impl fmt::Debug for ProxyEnvLayer {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 f.debug_struct("ProxyEnvLayer")
272 .field("schemes", &self.schemes)
273 .field("all", &self.all)
274 .field("load_error_policy", &self.load_error_policy)
275 .field("overwrite", &self.overwrite)
276 .finish()
277 }
278}
279
280impl ProxyEnvLayer {
281 #[must_use]
283 pub fn new() -> Self {
284 Self::new_with_reader(read_proxy_environment_variable)
285 }
286
287 #[must_use]
295 pub fn new_with_reader<F>(reader: F) -> Self
296 where
297 F: Fn(&str) -> Result<Option<String>, BoxError> + Send + Sync + 'static,
298 {
299 let reader: Arc<EnvironmentReader> = Arc::new(reader);
300 Self {
301 schemes: LazySchemeProxyAddresses::new(reader.clone()),
302 all: LazyProxyAddress::new(ALL_PROXY_ENV, reader),
303 load_error_policy: LoadErrorPolicy::Reject,
304 overwrite: false,
305 }
306 }
307
308 generate_set_and_with! {
309 pub fn http_proxy_env_vars(
314 mut self,
315 names: impl IntoIterator<Item = impl Into<Box<str>>>,
316 ) -> Self {
317 self.schemes.set_names(Protocol::HTTP, names);
318 self
319 }
320 }
321
322 generate_set_and_with! {
323 pub fn https_proxy_env_vars(
328 mut self,
329 names: impl IntoIterator<Item = impl Into<Box<str>>>,
330 ) -> Self {
331 self.schemes.set_names(Protocol::HTTPS, names);
332 self
333 }
334 }
335
336 generate_set_and_with! {
337 pub fn protocol_proxy_env_vars(
340 mut self,
341 protocol: Protocol,
342 names: impl IntoIterator<Item = impl Into<Box<str>>>,
343 ) -> Self {
344 self.schemes.set_names(protocol, names);
345 self
346 }
347 }
348
349 generate_set_and_with! {
350 pub fn all_proxy_env_vars(
355 mut self,
356 names: impl IntoIterator<Item = impl Into<Box<str>>>,
357 ) -> Self {
358 self.all.set_names(names);
359 self
360 }
361 }
362
363 generate_set_and_with! {
364 pub fn load_error_sink(mut self, sink: impl ErrorSink) -> Self {
370 self.load_error_policy = LoadErrorPolicy::Handle(Arc::new(sink));
371 self.schemes.reset();
372 self.all.reset();
373 self
374 }
375 }
376
377 generate_set_and_with! {
378 pub fn overwrite(mut self, overwrite: bool) -> Self {
381 self.overwrite = overwrite;
382 self
383 }
384 }
385
386 async fn proxy_for(&self, protocol: &Protocol) -> Result<Option<ProxyAddress>, BoxError> {
387 let specific = self.schemes.load(protocol, &self.load_error_policy).await?;
388 match specific {
389 Some(address) => Ok(Some(address)),
390 None => self.all.load(&self.load_error_policy).await,
391 }
392 }
393}
394
395impl Default for ProxyEnvLayer {
396 fn default() -> Self {
397 Self::new()
398 }
399}
400
401impl<S> Layer<S> for ProxyEnvLayer {
402 type Service = ProxyEnvService<S>;
403
404 fn layer(&self, inner: S) -> Self::Service {
405 ProxyEnvService {
406 inner,
407 layer: self.clone(),
408 }
409 }
410
411 fn into_layer(self, inner: S) -> Self::Service {
412 ProxyEnvService { inner, layer: self }
413 }
414}
415
416#[derive(Debug, Clone)]
418pub struct ProxyEnvService<S> {
419 inner: S,
420 layer: ProxyEnvLayer,
421}
422
423impl<S, Input> Service<Input> for ProxyEnvService<S>
424where
425 S: Service<Input, Error: Into<BoxError>>,
426 Input: UriInputExt + ProtocolInputExt + ExtensionsRef + Send + 'static,
427{
428 type Output = S::Output;
429 type Error = BoxError;
430
431 async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
432 if !self.layer.overwrite && is_already_routed(&input) {
433 return self.inner.serve(input).await.map_err(Into::into);
434 }
435 if let Some(address) = self.layer.proxy_for(&request_protocol(&input)).await? {
436 input.extensions().insert(ProxyRoute::Proxy(address));
437 }
438 self.inner.serve(input).await.map_err(Into::into)
439 }
440}
441
442#[derive(Clone)]
443struct LazyBypassRules {
444 names: Arc<[Box<str>]>,
445 reader: Arc<EnvironmentReader>,
446 cached: Arc<OnceCell<Result<Arc<[BypassRule]>, CachedLoadError>>>,
447}
448
449impl fmt::Debug for LazyBypassRules {
450 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451 f.debug_struct("LazyBypassRules")
452 .field("names", &self.names)
453 .field("cached", &self.cached.get())
454 .finish_non_exhaustive()
455 }
456}
457
458impl LazyBypassRules {
459 fn new(reader: Arc<EnvironmentReader>) -> Self {
460 Self {
461 names: default_env_names(NO_PROXY_ENV),
462 reader,
463 cached: Arc::new(OnceCell::new()),
464 }
465 }
466
467 fn set_names(&mut self, names: impl IntoIterator<Item = impl Into<Box<str>>>) {
468 self.names = env_names(names);
469 self.cached = Arc::new(OnceCell::new());
470 }
471
472 fn reset(&mut self) {
473 self.cached = Arc::new(OnceCell::new());
474 }
475
476 async fn load(&self, policy: &LoadErrorPolicy) -> Result<Arc<[BypassRule]>, BoxError> {
477 match self
478 .cached
479 .get_or_init(|| async {
480 match self.load_uncached(policy) {
481 Ok(rules) => Ok(rules),
482 Err(error) => policy.handle_cached(error, Arc::<[BypassRule]>::from([])),
483 }
484 })
485 .await
486 {
487 Ok(rules) => Ok(rules.clone()),
488 Err(error) => Err(Box::new(error.clone())),
489 }
490 }
491
492 fn load_uncached(&self, policy: &LoadErrorPolicy) -> Result<Arc<[BypassRule]>, BoxError> {
493 let Some((name, value)) = first_non_empty_value(&self.names, self.reader.as_ref())? else {
494 return Ok(Arc::new([]));
495 };
496 let mut rules = Vec::new();
497 for value in value
498 .split(',')
499 .map(str::trim)
500 .filter(|value| !value.is_empty())
501 {
502 match BypassRule::compile_with_dialect(value, BypassRuleDialect::NoProxy) {
503 Ok(rule) => rules.push(rule),
504 Err(error) => {
505 let error = error
506 .context("parse no-proxy environment variable")
507 .context_str_field("environment_variable", name);
508 policy.handle(error)?;
509 }
510 }
511 }
512 Ok(rules.into())
513 }
514}
515
516#[derive(Clone)]
535pub struct NoProxyEnvLayer {
536 rules: LazyBypassRules,
537 load_error_policy: LoadErrorPolicy,
538 overwrite: bool,
539}
540
541impl fmt::Debug for NoProxyEnvLayer {
542 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
543 f.debug_struct("NoProxyEnvLayer")
544 .field("rules", &self.rules)
545 .field("load_error_policy", &self.load_error_policy)
546 .field("overwrite", &self.overwrite)
547 .finish()
548 }
549}
550
551impl NoProxyEnvLayer {
552 #[must_use]
554 pub fn new() -> Self {
555 Self::new_with_reader(read_proxy_environment_variable)
556 }
557
558 #[must_use]
563 pub fn new_with_reader<F>(reader: F) -> Self
564 where
565 F: Fn(&str) -> Result<Option<String>, BoxError> + Send + Sync + 'static,
566 {
567 Self {
568 rules: LazyBypassRules::new(Arc::new(reader)),
569 load_error_policy: LoadErrorPolicy::Reject,
570 overwrite: false,
571 }
572 }
573
574 generate_set_and_with! {
575 pub fn no_proxy_env_vars(
580 mut self,
581 names: impl IntoIterator<Item = impl Into<Box<str>>>,
582 ) -> Self {
583 self.rules.set_names(names);
584 self
585 }
586 }
587
588 generate_set_and_with! {
589 pub fn load_error_sink(mut self, sink: impl ErrorSink) -> Self {
593 self.load_error_policy = LoadErrorPolicy::Handle(Arc::new(sink));
594 self.rules.reset();
595 self
596 }
597 }
598
599 generate_set_and_with! {
600 pub fn overwrite(mut self, overwrite: bool) -> Self {
604 self.overwrite = overwrite;
605 self
606 }
607 }
608}
609
610impl Default for NoProxyEnvLayer {
611 fn default() -> Self {
612 Self::new()
613 }
614}
615
616impl<S> Layer<S> for NoProxyEnvLayer {
617 type Service = NoProxyEnvService<S>;
618
619 fn layer(&self, inner: S) -> Self::Service {
620 NoProxyEnvService {
621 inner,
622 layer: self.clone(),
623 }
624 }
625
626 fn into_layer(self, inner: S) -> Self::Service {
627 NoProxyEnvService { inner, layer: self }
628 }
629}
630
631#[derive(Debug, Clone)]
633pub struct NoProxyEnvService<S> {
634 inner: S,
635 layer: NoProxyEnvLayer,
636}
637
638impl<S, Input> Service<Input> for NoProxyEnvService<S>
639where
640 S: Service<Input, Error: Into<BoxError>>,
641 Input: UriInputExt + AuthorityInputExt + ProtocolInputExt + ExtensionsRef + Send + 'static,
642{
643 type Output = S::Output;
644 type Error = BoxError;
645
646 async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
647 if !self.layer.overwrite && is_already_routed(&input) {
648 return self.inner.serve(input).await.map_err(Into::into);
649 }
650 let rules = self.layer.rules.load(&self.layer.load_error_policy).await?;
651 if !rules.is_empty() && no_proxy_matches_input(&rules, &input) {
652 input.extensions().insert(ProxyRoute::Direct);
653 }
654 self.inner.serve(input).await.map_err(Into::into)
655 }
656}
657
658fn no_proxy_matches_input<I>(rules: &[BypassRule], input: &I) -> bool
659where
660 I: UriInputExt + AuthorityInputExt + ProtocolInputExt,
661{
662 let protocol = request_protocol(input);
663 let default_port = protocol.default_port();
664 let uri = input.uri();
665 if let Some(host) = uri.host() {
666 return super::bypass::matches_any_rule(
667 rules,
668 Some(&protocol),
669 host,
670 uri.port_u16().or(default_port),
671 );
672 }
673 input.authority().is_some_and(|authority| {
674 super::bypass::matches_any_rule(
675 rules,
676 Some(&protocol),
677 authority.host.view(),
678 authority.port_u16().or(default_port),
679 )
680 })
681}
682
683#[cfg(test)]
684mod tests {
685 use std::{
686 convert::Infallible,
687 sync::atomic::{AtomicUsize, Ordering},
688 time::Duration,
689 };
690
691 use ahash::HashMap;
692 use parking_lot::Mutex;
693 use rama_core::{extensions::Extensions, service::service_fn};
694
695 use crate::{
696 address::{HostWithOptPort, ProxyAddress},
697 client::ProxyRoutes,
698 uri::Uri,
699 };
700
701 use super::*;
702
703 #[derive(Debug, Clone)]
704 struct TestInput {
705 uri: Uri,
706 protocol: Option<Protocol>,
707 authority: Option<HostWithOptPort>,
708 extensions: Extensions,
709 }
710
711 impl TestInput {
712 fn new(uri: &str) -> Self {
713 Self {
714 uri: uri.parse().unwrap(),
715 protocol: None,
716 authority: None,
717 extensions: Extensions::new(),
718 }
719 }
720
721 fn with_route(self, route: ProxyRoute) -> Self {
722 self.extensions.insert(route);
723 self
724 }
725 }
726
727 impl UriInputExt for TestInput {
728 fn uri(&self) -> &Uri {
729 &self.uri
730 }
731 }
732
733 impl ProtocolInputExt for TestInput {
734 fn protocol(&self) -> Option<&Protocol> {
735 self.protocol.as_ref().or_else(|| self.uri.scheme())
736 }
737 }
738
739 impl AuthorityInputExt for TestInput {
740 fn authority(&self) -> Option<HostWithOptPort> {
741 self.authority.clone().or_else(|| {
742 self.uri
743 .authority()
744 .map(|authority| authority.into_owned().address)
745 })
746 }
747 }
748
749 impl ExtensionsRef for TestInput {
750 fn extensions(&self) -> &Extensions {
751 &self.extensions
752 }
753 }
754
755 type SeenRoutes = Arc<Mutex<Vec<Option<ProxyRoute>>>>;
756
757 fn recorder() -> (
758 impl Service<TestInput, Output = (), Error = Infallible> + Clone,
759 SeenRoutes,
760 ) {
761 let seen = Arc::new(Mutex::new(Vec::new()));
762 let service = crate::client::ProxyRoutesLayer::new().into_layer(service_fn({
763 let seen = seen.clone();
764 move |input: TestInput| {
765 let route = input.extensions.get_ref::<ProxyRoute>().cloned();
766 seen.lock().push(route);
767 async { Ok::<_, Infallible>(()) }
768 }
769 }));
770 (service, seen)
771 }
772
773 type EnvReads = Arc<Mutex<Vec<String>>>;
774
775 fn environment(
776 values: impl IntoIterator<Item = (&'static str, &'static str)>,
777 ) -> (
778 impl Fn(&str) -> Result<Option<String>, BoxError> + Send + Sync + 'static,
779 EnvReads,
780 ) {
781 let values = Arc::new(
782 values
783 .into_iter()
784 .map(|(name, value)| (name.to_owned(), value.to_owned()))
785 .collect::<HashMap<_, _>>(),
786 );
787 let reads = Arc::new(Mutex::new(Vec::new()));
788 let reader = {
789 let reads = reads.clone();
790 move |name: &str| {
791 reads.lock().push(name.to_owned());
792 Ok(values.get(name).cloned())
793 }
794 };
795 (reader, reads)
796 }
797
798 fn proxy_host(route: Option<&ProxyRoute>) -> Option<String> {
799 route
800 .and_then(ProxyRoute::proxy_address)
801 .map(|address| address.address.host.to_string())
802 }
803
804 #[test]
805 fn process_environment_reader_rejects_invalid_names() {
806 for name in ["", "RAMA=PROXY", "RAMA\0PROXY"] {
807 read_proxy_environment_variable(name).unwrap_err();
808 }
809 assert_eq!(
810 read_proxy_environment_variable("RAMA_PROXY_ENV_TEST_DEFINITELY_ABSENT_9D72B4")
811 .unwrap(),
812 None,
813 );
814 assert_eq!(parse_proxy_address_env_value(None).unwrap(), None);
815 assert_eq!(parse_proxy_address_env_value(Some(" ")).unwrap(), None);
816 assert_eq!(
817 parse_proxy_address_env_value(Some(" http://proxy.example:8080 "))
818 .unwrap()
819 .unwrap()
820 .address
821 .host
822 .to_str(),
823 "proxy.example"
824 );
825 parse_proxy_address_env_value(Some("http://")).unwrap_err();
826 }
827
828 #[test]
829 fn portless_proxy_values_use_the_protocol_proxy_port() {
830 for (value, protocol, port) in [
831 (
832 "proxy.example",
833 Protocol::HTTP,
834 Protocol::HTTP_PROXY_DEFAULT_PORT,
835 ),
836 (
837 "http://proxy.example",
838 Protocol::HTTP,
839 Protocol::HTTP_PROXY_DEFAULT_PORT,
840 ),
841 (
842 "https://proxy.example",
843 Protocol::HTTPS,
844 Protocol::HTTPS_DEFAULT_PORT,
845 ),
846 (
847 "socks5://proxy.example",
848 Protocol::SOCKS5,
849 Protocol::SOCKS5_DEFAULT_PORT,
850 ),
851 ] {
852 let proxy = parse_proxy_address_env_value(Some(value)).unwrap().unwrap();
853 assert_eq!(proxy.protocol, Some(protocol), "{value}");
854 assert_eq!(proxy.address.port, port, "{value}");
855 }
856
857 let proxy = parse_proxy_address_env_value(Some("user:pass@proxy.example:3128"))
858 .unwrap()
859 .unwrap();
860 assert_eq!(proxy.protocol, Some(Protocol::HTTP));
861 assert_eq!(proxy.address.port, 3128);
862 let Some(ProxyCredential::Basic(basic)) = proxy.credential else {
863 panic!("expected Basic proxy credentials")
864 };
865 assert_eq!(basic.username(), "user");
866
867 for value in [
868 "http://proxy.example:80",
869 "https://proxy.example:443",
870 "socks5://proxy.example:1081",
871 "http://user:pass@[2001:db8::1]:3128",
872 ] {
873 let expected = value.rsplit_once(':').unwrap().1.parse::<u16>().unwrap();
874 let proxy = parse_proxy_address_env_value(Some(value)).unwrap().unwrap();
875 assert_eq!(proxy.address.port, expected, "{value}");
876 }
877 }
878
879 #[tokio::test]
880 async fn custom_scheme_cache_is_bounded() {
881 let schemes = LazySchemeProxyAddresses::new(Arc::new(|_| Ok(None)));
882 for index in 0..(MAX_CACHED_PROXY_SCHEMES * 4) {
883 let protocol: Protocol = format!("custom{index}").parse().unwrap();
884 assert!(
885 schemes
886 .load(&protocol, &LoadErrorPolicy::Reject)
887 .await
888 .unwrap()
889 .is_none()
890 );
891 }
892 schemes.cached.run_pending_tasks();
893 assert!(schemes.cached.entry_count() <= MAX_CACHED_PROXY_SCHEMES);
894 }
895
896 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
897 async fn concurrent_requests_share_one_scheme_environment_load() {
898 const REQUESTS: usize = 64;
899
900 let reads = Arc::new(AtomicUsize::new(0));
901 let inner = service_fn(|input: TestInput| async move {
902 assert_eq!(
903 proxy_host(input.extensions.get_ref::<ProxyRoute>()).as_deref(),
904 Some("shared.proxy")
905 );
906 Ok::<_, Infallible>(())
907 });
908 let service = ProxyEnvLayer::new_with_reader({
909 let reads = reads.clone();
910 move |name| {
911 assert_eq!(name, "http_proxy");
912 reads.fetch_add(1, Ordering::AcqRel);
913 std::thread::sleep(Duration::from_millis(20));
916 Ok(Some("http://shared.proxy:8080".to_owned()))
917 }
918 })
919 .into_layer(inner);
920 let barrier = Arc::new(tokio::sync::Barrier::new(REQUESTS));
921 let tasks = (0..REQUESTS)
922 .map(|_| {
923 let barrier = barrier.clone();
924 let service = service.clone();
925 tokio::spawn(async move {
926 barrier.wait().await;
927 service
928 .serve(TestInput::new("http://example.com/"))
929 .await
930 .unwrap();
931 })
932 })
933 .collect::<Vec<_>>();
934 for task in tasks {
935 task.await.unwrap();
936 }
937
938 assert_eq!(reads.load(Ordering::Acquire), 1);
939 }
940
941 #[tokio::test]
942 async fn proxy_variables_follow_curl_precedence_and_load_lazily() {
943 let (reader, reads) = environment([
944 ("http_proxy", "http://http.proxy:8080"),
945 ("HTTP_PROXY", "http://unsafe.proxy:8080"),
946 ("HTTPS_PROXY", "http://https.proxy:8443"),
947 ("ALL_PROXY", "socks5h://all.proxy:1080"),
948 ]);
949 let (inner, seen) = recorder();
950 let service = ProxyEnvLayer::new_with_reader(reader).into_layer(inner);
951
952 service
953 .serve(TestInput::new("http://example.com/"))
954 .await
955 .unwrap();
956 service
957 .serve(TestInput::new("https://example.com/"))
958 .await
959 .unwrap();
960 service
961 .serve(TestInput::new("ftp://example.com/file"))
962 .await
963 .unwrap();
964 service
965 .serve(TestInput::new("ws://example.com/socket"))
966 .await
967 .unwrap();
968
969 let seen = seen.lock();
970 assert_eq!(proxy_host(seen[0].as_ref()).as_deref(), Some("http.proxy"));
971 assert_eq!(proxy_host(seen[1].as_ref()).as_deref(), Some("https.proxy"));
972 assert_eq!(proxy_host(seen[2].as_ref()).as_deref(), Some("all.proxy"));
973 assert_eq!(proxy_host(seen[3].as_ref()).as_deref(), Some("all.proxy"));
974 assert_eq!(
975 reads.lock().as_slice(),
976 [
977 "http_proxy",
978 "https_proxy",
979 "HTTPS_PROXY",
980 "ftp_proxy",
981 "FTP_PROXY",
982 "all_proxy",
983 "ALL_PROXY",
984 "ws_proxy",
985 "WS_PROXY",
986 ]
987 );
988 }
989
990 #[tokio::test]
991 async fn http_proxy_is_not_an_https_fallback() {
992 let (reader, reads) = environment([("http_proxy", "http://http.proxy:8080")]);
993 let (inner, seen) = recorder();
994 let service = ProxyEnvLayer::new_with_reader(reader).into_layer(inner);
995
996 service
997 .serve(TestInput::new("https://example.com/"))
998 .await
999 .unwrap();
1000 service
1001 .serve(TestInput::new("http://example.com/"))
1002 .await
1003 .unwrap();
1004
1005 assert!(seen.lock()[0].is_none());
1006 assert_eq!(
1007 proxy_host(seen.lock()[1].as_ref()).as_deref(),
1008 Some("http.proxy")
1009 );
1010 assert_eq!(
1011 reads.lock().as_slice(),
1012 [
1013 "https_proxy",
1014 "HTTPS_PROXY",
1015 "all_proxy",
1016 "ALL_PROXY",
1017 "http_proxy"
1018 ]
1019 );
1020 }
1021
1022 #[tokio::test]
1023 async fn malformed_proxy_group_is_not_parsed_for_another_scheme() {
1024 let (reader, reads) = environment([
1025 ("http_proxy", "http://"),
1026 ("HTTPS_PROXY", "http://secure.proxy:8443"),
1027 ]);
1028 let (inner, seen) = recorder();
1029
1030 ProxyEnvLayer::new_with_reader(reader)
1031 .into_layer(inner)
1032 .serve(TestInput::new("https://example.com/"))
1033 .await
1034 .unwrap();
1035
1036 assert_eq!(
1037 proxy_host(seen.lock()[0].as_ref()).as_deref(),
1038 Some("secure.proxy")
1039 );
1040 assert_eq!(reads.lock().as_slice(), ["https_proxy", "HTTPS_PROXY"]);
1041 }
1042
1043 #[tokio::test]
1044 async fn empty_lowercase_value_falls_through_to_uppercase() {
1045 let (reader, reads) = environment([
1046 ("https_proxy", " "),
1047 ("HTTPS_PROXY", "http://upper.proxy:8443"),
1048 ]);
1049 let (inner, seen) = recorder();
1050
1051 ProxyEnvLayer::new_with_reader(reader)
1052 .into_layer(inner)
1053 .serve(TestInput::new("https://example.com/"))
1054 .await
1055 .unwrap();
1056
1057 assert_eq!(
1058 proxy_host(seen.lock()[0].as_ref()).as_deref(),
1059 Some("upper.proxy")
1060 );
1061 assert_eq!(reads.lock().as_slice(), ["https_proxy", "HTTPS_PROXY"]);
1062 }
1063
1064 #[tokio::test]
1065 async fn non_empty_lowercase_value_wins_over_uppercase() {
1066 let (reader, reads) = environment([
1067 ("https_proxy", "http://lower.proxy:8443"),
1068 ("HTTPS_PROXY", "http://upper.proxy:8443"),
1069 ]);
1070 let (inner, seen) = recorder();
1071
1072 ProxyEnvLayer::new_with_reader(reader)
1073 .into_layer(inner)
1074 .serve(TestInput::new("https://example.com/"))
1075 .await
1076 .unwrap();
1077
1078 assert_eq!(
1079 proxy_host(seen.lock()[0].as_ref()).as_deref(),
1080 Some("lower.proxy")
1081 );
1082 assert_eq!(reads.lock().as_slice(), ["https_proxy"]);
1083 }
1084
1085 #[tokio::test]
1086 async fn proxy_variable_names_are_customizable_and_groups_can_be_disabled() {
1087 let (reader, reads) = environment([("RAMA_PROXY", "socks5://custom.proxy:1080")]);
1088 let (inner, seen) = recorder();
1089 let service = ProxyEnvLayer::new_with_reader(reader)
1090 .with_http_proxy_env_vars(["RAMA_PROXY"])
1091 .with_https_proxy_env_vars([] as [&str; 0])
1092 .with_all_proxy_env_vars([] as [&str; 0])
1093 .into_layer(inner);
1094
1095 service
1096 .serve(TestInput::new("http://example.com/"))
1097 .await
1098 .unwrap();
1099 service
1100 .serve(TestInput::new("https://example.com/"))
1101 .await
1102 .unwrap();
1103
1104 let seen = seen.lock();
1105 assert_eq!(
1106 proxy_host(seen[0].as_ref()).as_deref(),
1107 Some("custom.proxy")
1108 );
1109 assert!(seen[1].is_none());
1110 assert_eq!(reads.lock().as_slice(), ["RAMA_PROXY"]);
1111 }
1112
1113 #[tokio::test]
1114 async fn websocket_and_custom_protocols_use_their_own_lazy_groups() {
1115 let (reader, reads) = environment([
1116 ("WS_PROXY", "http://websocket.proxy:8080"),
1117 ("git_proxy", "socks5://git.proxy:1080"),
1118 ("ALL_PROXY", "http://fallback.proxy:8080"),
1119 ]);
1120 let (inner, seen) = recorder();
1121 let service = ProxyEnvLayer::new_with_reader(reader).into_layer(inner);
1122
1123 service
1124 .serve(TestInput::new("ws://example.com/socket"))
1125 .await
1126 .unwrap();
1127 service
1128 .serve(TestInput::new("git://example.com/repository"))
1129 .await
1130 .unwrap();
1131
1132 assert_eq!(
1133 seen.lock()
1134 .iter()
1135 .map(|route| proxy_host(route.as_ref()).unwrap())
1136 .collect::<Vec<_>>(),
1137 ["websocket.proxy", "git.proxy"]
1138 );
1139 assert_eq!(
1140 reads.lock().as_slice(),
1141 ["ws_proxy", "WS_PROXY", "git_proxy"]
1142 );
1143 }
1144
1145 #[tokio::test]
1146 async fn proxy_load_errors_reject_by_default_and_are_cached() {
1147 let (reader, reads) = environment([("http_proxy", "http://")]);
1148 let (inner, _) = recorder();
1149 let service = ProxyEnvLayer::new_with_reader(reader).into_layer(inner);
1150
1151 for _ in 0..2 {
1152 service
1153 .serve(TestInput::new("http://example.com/"))
1154 .await
1155 .unwrap_err();
1156 }
1157 assert_eq!(reads.lock().as_slice(), ["http_proxy"]);
1158 }
1159
1160 #[tokio::test]
1161 async fn handled_proxy_error_falls_back_and_sinks_once() {
1162 let (reader, reads) = environment([
1163 ("http_proxy", "http://"),
1164 ("all_proxy", "socks5://fallback.proxy:1080"),
1165 ]);
1166 let sink_calls = Arc::new(Mutex::new(Vec::new()));
1167 let (inner, seen) = recorder();
1168 let service = ProxyEnvLayer::new_with_reader(reader)
1169 .with_load_error_sink({
1170 let sink_calls = sink_calls.clone();
1171 move |error: BoxError| sink_calls.lock().push(error.to_string())
1172 })
1173 .into_layer(inner);
1174
1175 for _ in 0..2 {
1176 service
1177 .serve(TestInput::new("http://example.com/"))
1178 .await
1179 .unwrap();
1180 }
1181
1182 assert_eq!(sink_calls.lock().len(), 1);
1183 assert_eq!(reads.lock().as_slice(), ["http_proxy", "all_proxy"]);
1184 assert!(
1185 seen.lock()
1186 .iter()
1187 .all(|route| proxy_host(route.as_ref()).as_deref() == Some("fallback.proxy"))
1188 );
1189 }
1190
1191 #[tokio::test]
1192 async fn preserved_route_avoids_every_environment_read() {
1193 let (reader, reads) = environment([("http_proxy", "http://env.proxy:8080")]);
1194 let (inner, seen) = recorder();
1195
1196 ProxyEnvLayer::new_with_reader(reader)
1197 .into_layer(inner)
1198 .serve(
1199 TestInput::new("http://example.com/").with_route(ProxyRoute::Proxy(
1200 "http://explicit.proxy:8080"
1201 .parse::<ProxyAddress>()
1202 .unwrap(),
1203 )),
1204 )
1205 .await
1206 .unwrap();
1207
1208 assert!(reads.lock().is_empty());
1209 assert_eq!(
1210 proxy_host(seen.lock()[0].as_ref()).as_deref(),
1211 Some("explicit.proxy")
1212 );
1213 }
1214
1215 #[tokio::test]
1216 async fn no_proxy_domains_and_networks_use_environment_patterns() {
1217 let (reader, reads) = environment([("NO_PROXY", ".example.com,10.0.0.0/8")]);
1218 let (inner, seen) = recorder();
1219 let service = NoProxyEnvLayer::new_with_reader(reader).into_layer(inner);
1220
1221 for uri in [
1222 "http://example.com/",
1223 "http://api.example.com/",
1224 "http://nonexample.com/",
1225 "http://10.2.3.4/",
1226 "http://11.2.3.4/",
1227 ] {
1228 service.serve(TestInput::new(uri)).await.unwrap();
1229 }
1230
1231 assert_eq!(
1232 seen.lock()
1233 .iter()
1234 .map(|route| route == &Some(ProxyRoute::Direct))
1235 .collect::<Vec<_>>(),
1236 [true, true, false, true, false]
1237 );
1238 assert_eq!(reads.lock().as_slice(), ["no_proxy", "NO_PROXY"]);
1239 }
1240
1241 #[tokio::test]
1242 async fn no_proxy_matches_mapped_ipv4_and_rooted_fqdns() {
1243 let (reader, _) = environment([("NO_PROXY", "10.0.0.0/8,api-*.example.com")]);
1244 let (inner, seen) = recorder();
1245 let service = NoProxyEnvLayer::new_with_reader(reader).into_layer(inner);
1246
1247 for uri in [
1248 "http://[::ffff:10.2.3.4]/",
1249 "http://[::ffff:11.2.3.4]/",
1250 "http://api-one.example.com./",
1251 ] {
1252 service.serve(TestInput::new(uri)).await.unwrap();
1253 }
1254
1255 assert_eq!(
1256 seen.lock()
1257 .iter()
1258 .map(|route| route == &Some(ProxyRoute::Direct))
1259 .collect::<Vec<_>>(),
1260 [true, false, true]
1261 );
1262 }
1263
1264 #[tokio::test]
1265 async fn no_proxy_plain_domains_match_descendants_and_globs_are_supported() {
1266 let (reader, _) = environment([("no_proxy", "exact.example,api-*.example")]);
1267 let (inner, seen) = recorder();
1268 let service = NoProxyEnvLayer::new_with_reader(reader).into_layer(inner);
1269
1270 for uri in [
1271 "http://exact.example/",
1272 "http://child.exact.example/",
1273 "http://api-v1.example/",
1274 "http://www.example/",
1275 ] {
1276 service.serve(TestInput::new(uri)).await.unwrap();
1277 }
1278
1279 assert_eq!(
1280 seen.lock()
1281 .iter()
1282 .map(|route| route == &Some(ProxyRoute::Direct))
1283 .collect::<Vec<_>>(),
1284 [true, true, true, false]
1285 );
1286 }
1287
1288 #[tokio::test]
1289 async fn lowercase_no_proxy_wins_over_uppercase() {
1290 let (reader, reads) =
1291 environment([("no_proxy", "lower.example"), ("NO_PROXY", "upper.example")]);
1292 let (inner, seen) = recorder();
1293 let service = NoProxyEnvLayer::new_with_reader(reader).into_layer(inner);
1294
1295 service
1296 .serve(TestInput::new("http://upper.example/"))
1297 .await
1298 .unwrap();
1299 service
1300 .serve(TestInput::new("http://lower.example/"))
1301 .await
1302 .unwrap();
1303
1304 assert!(seen.lock()[0].is_none());
1305 assert_eq!(seen.lock()[1], Some(ProxyRoute::Direct));
1306 assert_eq!(reads.lock().as_slice(), ["no_proxy"]);
1307 }
1308
1309 #[tokio::test]
1310 async fn no_proxy_variable_names_are_customizable() {
1311 let (reader, reads) = environment([
1312 ("no_proxy", "lower.example"),
1313 ("NO_PROXY", "upper.example"),
1314 ("RAMA_NO_PROXY", "custom.example"),
1315 ]);
1316 let (inner, seen) = recorder();
1317 let service = NoProxyEnvLayer::new_with_reader(reader)
1318 .with_no_proxy_env_vars(["RAMA_NO_PROXY"])
1319 .into_layer(inner);
1320
1321 service
1322 .serve(TestInput::new("http://custom.example/"))
1323 .await
1324 .unwrap();
1325 service
1326 .serve(TestInput::new("http://lower.example/"))
1327 .await
1328 .unwrap();
1329
1330 assert_eq!(seen.lock()[0], Some(ProxyRoute::Direct));
1331 assert!(seen.lock()[1].is_none());
1332 assert_eq!(reads.lock().as_slice(), ["RAMA_NO_PROXY"]);
1333 }
1334
1335 #[tokio::test]
1336 async fn no_proxy_single_wildcard_matches_every_host() {
1337 let (reader, _) = environment([("no_proxy", "*")]);
1338 let (inner, seen) = recorder();
1339 let service = NoProxyEnvLayer::new_with_reader(reader).into_layer(inner);
1340
1341 for uri in ["http://example.com/", "https://192.0.2.1/"] {
1342 service.serve(TestInput::new(uri)).await.unwrap();
1343 }
1344
1345 assert!(
1346 seen.lock()
1347 .iter()
1348 .all(|route| route == &Some(ProxyRoute::Direct))
1349 );
1350 }
1351
1352 #[tokio::test]
1353 async fn no_proxy_port_rules_use_the_destination_default_port() {
1354 let (reader, _) = environment([("no_proxy", "port.example:80")]);
1355 let (inner, seen) = recorder();
1356 let service = NoProxyEnvLayer::new_with_reader(reader).into_layer(inner);
1357
1358 for uri in [
1359 "http://port.example/",
1360 "http://port.example:8080/",
1361 "https://port.example/",
1362 ] {
1363 service.serve(TestInput::new(uri)).await.unwrap();
1364 }
1365
1366 assert_eq!(
1367 seen.lock()
1368 .iter()
1369 .map(|route| route == &Some(ProxyRoute::Direct))
1370 .collect::<Vec<_>>(),
1371 [true, false, false]
1372 );
1373 }
1374
1375 #[tokio::test]
1376 async fn no_proxy_passes_hostless_inputs_through() {
1377 let (reader, _) = environment([("no_proxy", "*")]);
1378 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1379 let service = NoProxyEnvLayer::new_with_reader(reader).into_layer(service_fn({
1380 let calls = calls.clone();
1381 move |_input: TestInput| {
1382 calls.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
1383 async { Ok::<_, Infallible>(()) }
1384 }
1385 }));
1386
1387 service.serve(TestInput::new("*")).await.unwrap();
1388 assert_eq!(calls.load(std::sync::atomic::Ordering::Acquire), 1);
1389 }
1390
1391 #[tokio::test]
1392 async fn no_proxy_preserves_existing_routes_without_reading_environment() {
1393 let reads = Arc::new(Mutex::new(Vec::new()));
1394 let (inner, seen) = recorder();
1395 let service = NoProxyEnvLayer::new_with_reader({
1396 let reads = reads.clone();
1397 move |name| {
1398 reads.lock().push(name.to_owned());
1399 Ok(Some("*".to_owned()))
1400 }
1401 })
1402 .into_layer(inner);
1403
1404 service
1405 .serve(
1406 TestInput::new("http://example.com/").with_route(ProxyRoute::Proxy(
1407 "http://explicit.proxy:8080".parse().unwrap(),
1408 )),
1409 )
1410 .await
1411 .unwrap();
1412
1413 assert!(reads.lock().is_empty());
1414 assert_eq!(
1415 proxy_host(seen.lock()[0].as_ref()).as_deref(),
1416 Some("explicit.proxy")
1417 );
1418 }
1419
1420 #[tokio::test]
1421 async fn no_proxy_can_overwrite_an_existing_route() {
1422 let (reader, _) = environment([("no_proxy", "*")]);
1423 let (inner, seen) = recorder();
1424
1425 NoProxyEnvLayer::new_with_reader(reader)
1426 .with_overwrite(true)
1427 .into_layer(inner)
1428 .serve(
1429 TestInput::new("http://example.com/").with_route(ProxyRoute::Proxy(
1430 "http://explicit.proxy:8080".parse().unwrap(),
1431 )),
1432 )
1433 .await
1434 .unwrap();
1435
1436 assert_eq!(seen.lock()[0], Some(ProxyRoute::Direct));
1437 }
1438
1439 #[tokio::test]
1440 async fn overwrite_replaces_an_authoritative_route_plan() {
1441 let old_routes = ProxyRoutes::new([
1442 ProxyRoute::Proxy("http://old.proxy:8080".parse().unwrap()),
1443 ProxyRoute::Direct,
1444 ]);
1445
1446 let (proxy_reader, _) = environment([("http_proxy", "http://new.proxy:8080")]);
1447 let (inner, seen) = recorder();
1448 let proxy_service = ProxyEnvLayer::new_with_reader(proxy_reader)
1449 .with_overwrite(true)
1450 .into_layer(inner);
1451 let input = TestInput::new("http://example.com/");
1452 input.extensions.insert(old_routes.clone());
1453 proxy_service.serve(input).await.unwrap();
1454 assert_eq!(
1455 proxy_host(seen.lock()[0].as_ref()).as_deref(),
1456 Some("new.proxy")
1457 );
1458
1459 let (bypass_reader, _) = environment([("no_proxy", "*")]);
1460 let (inner, seen) = recorder();
1461 let bypass_service = NoProxyEnvLayer::new_with_reader(bypass_reader)
1462 .with_overwrite(true)
1463 .into_layer(inner);
1464 let input = TestInput::new("http://example.com/");
1465 input.extensions.insert(old_routes);
1466 bypass_service.serve(input).await.unwrap();
1467 assert_eq!(seen.lock()[0], Some(ProxyRoute::Direct));
1468 }
1469
1470 #[tokio::test]
1471 async fn invalid_no_proxy_rules_reject_atomically_by_default() {
1472 let (reader, reads) = environment([("no_proxy", "example.com,.not a valid domain")]);
1473 let (inner, _) = recorder();
1474 let service = NoProxyEnvLayer::new_with_reader(reader).into_layer(inner);
1475
1476 for _ in 0..2 {
1477 service
1478 .serve(TestInput::new("http://example.com/"))
1479 .await
1480 .unwrap_err();
1481 }
1482 assert_eq!(reads.lock().as_slice(), ["no_proxy"]);
1483 }
1484
1485 #[tokio::test]
1486 async fn handled_invalid_no_proxy_rule_keeps_valid_rules() {
1487 let (reader, reads) = environment([("no_proxy", "example.com,.not a valid domain")]);
1488 let sink_calls = Arc::new(Mutex::new(Vec::new()));
1489 let (inner, seen) = recorder();
1490 let service = NoProxyEnvLayer::new_with_reader(reader)
1491 .with_load_error_sink({
1492 let sink_calls = sink_calls.clone();
1493 move |error: BoxError| sink_calls.lock().push(error.to_string())
1494 })
1495 .into_layer(inner);
1496
1497 service
1498 .serve(TestInput::new("http://example.com/"))
1499 .await
1500 .unwrap();
1501 service
1502 .serve(TestInput::new("http://example.net/"))
1503 .await
1504 .unwrap();
1505
1506 assert_eq!(sink_calls.lock().len(), 1);
1507 assert_eq!(reads.lock().as_slice(), ["no_proxy"]);
1508 assert_eq!(seen.lock()[0], Some(ProxyRoute::Direct));
1509 assert!(seen.lock()[1].is_none());
1510 }
1511}