Skip to main content

rama_net/client/proxy/
env.rs

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/// Lazily select a proxy from curl-compatible environment variables.
245///
246/// HTTP requests use lowercase `http_proxy`; uppercase `HTTP_PROXY` is never
247/// read because CGI turns an incoming `Proxy` header into that variable.
248/// Every request first tries its URL scheme's lowercase and uppercase proxy
249/// variables, then `all_proxy` and `ALL_PROXY`. HTTP is the security-sensitive
250/// exception: only lowercase `http_proxy` is accepted. For example, WebSocket
251/// requests use `ws_proxy` / `WS_PROXY`, not `http_proxy`.
252///
253/// Every variable group has an independent, shared cache. A request only reads
254/// the group it needs, and `ALL_PROXY` is not read when a scheme-specific proxy
255/// exists. Empty name lists disable the corresponding group.
256///
257/// See curl's [proxy environment variable documentation][curl-env] for the
258/// interoperability rules this layer follows.
259///
260/// [curl-env]: https://everything.curl.dev/usingcurl/proxies/env.html
261#[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    /// Create a lazy layer backed by the process environment.
282    #[must_use]
283    pub fn new() -> Self {
284        Self::new_with_reader(read_proxy_environment_variable)
285    }
286
287    /// Create a lazy layer backed by a custom environment reader.
288    ///
289    /// This is useful for applications which virtualize their environment and
290    /// for deterministic tests. `Ok(None)` means that the name is not defined.
291    /// The reader runs synchronously during the first request for a variable
292    /// group and should therefore avoid blocking I/O; concurrent callers await
293    /// the same cached initialization.
294    #[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        /// Replace the ordered HTTP proxy environment-variable names.
310        ///
311        /// The default is only `http_proxy`. Supplying no names disables this
312        /// scheme-specific lookup.
313        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        /// Replace the ordered HTTPS proxy environment-variable names.
324        ///
325        /// The default is `https_proxy`, then `HTTPS_PROXY`. Supplying no names
326        /// disables this scheme-specific lookup.
327        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        /// Replace the ordered proxy environment-variable names for one URL
338        /// protocol. Supplying no names disables its scheme-specific lookup.
339        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        /// Replace the ordered all-protocol proxy environment-variable names.
351        ///
352        /// The default is `all_proxy`, then `ALL_PROXY`. Supplying no names
353        /// disables the fallback entirely.
354        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        /// Handle each lazy load error through an [`ErrorSink`] and treat that
365        /// variable group as absent. By default errors reject the request.
366        ///
367        /// The handled result is cached, so a sink is called at most once for
368        /// each variable group.
369        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        /// Replace an existing [`ProxyRoute`] or
379        /// [`ProxyRoutes`][crate::client::ProxyRoutes] decision.
380        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/// Service produced by [`ProxyEnvLayer`].
417#[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/// Lazily apply conventional `NO_PROXY` bypass rules.
517///
518/// Lowercase `no_proxy` takes precedence over uppercase `NO_PROXY`. Entries
519/// are comma-separated. As in curl, wget, Go and Python, a plain domain such as
520/// `example.com` matches its apex and descendants. Leading-dot and `*.` forms
521/// have the same subtree behavior. Rama additionally accepts arbitrary host
522/// globs; a single `*` matches every host, and IP addresses and CIDR networks
523/// use typed address matching. When a rule matches, the layer inserts
524/// [`ProxyRoute::Direct`].
525///
526/// Place this layer before explicit, environment, and system proxy layers to
527/// give bypass rules priority without enabling route overwrites.
528///
529/// The environment-variable names and core matching behavior follow curl's
530/// [proxy environment variable documentation][curl-env]. Rama's arbitrary
531/// glob support is a backwards-compatible extension of that shared convention.
532///
533/// [curl-env]: https://everything.curl.dev/usingcurl/proxies/env.html
534#[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    /// Create a lazy layer backed by the process environment.
553    #[must_use]
554    pub fn new() -> Self {
555        Self::new_with_reader(read_proxy_environment_variable)
556    }
557
558    /// Create a lazy layer backed by a custom environment reader.
559    ///
560    /// The reader runs synchronously during the first request and should avoid
561    /// blocking I/O; concurrent callers await the same cached initialization.
562    #[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        /// Replace the ordered no-proxy environment-variable names.
576        ///
577        /// The default is `no_proxy`, then `NO_PROXY`. Supplying no names
578        /// disables environment bypass rules.
579        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        /// Send invalid rules or environment-read errors to an [`ErrorSink`].
590        /// Invalid individual rules are omitted while valid rules remain in
591        /// effect. By default any such error rejects the request.
592        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        /// Replace an existing [`ProxyRoute`] or
601        /// [`ProxyRoutes`][crate::client::ProxyRoutes] decision with a direct
602        /// route when a no-proxy rule matches.
603        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/// Service produced by [`NoProxyEnvLayer`].
632#[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                // Keep the first initialization open long enough for the
914                // other tasks to exercise the shared async OnceCell.
915                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}