Skip to main content

stygian_proxy/
routing.rs

1//! Protocol-aware routing path resolution.
2//!
3//! [`resolve_routing_path`] translates a proxy's advertised capabilities and
4//! the caller's transport preference into a concrete [`RoutingPath`] that the
5//! HTTP client layer uses when opening a connection.
6
7use crate::types::{ProxyCapabilities, RoutingPath};
8
9/// Preference expressed by the caller when acquiring a proxy.
10///
11/// # Example
12/// ```
13/// use stygian_proxy::routing::TransportPreference;
14/// assert_eq!(TransportPreference::default(), TransportPreference::PreferH3);
15/// ```
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum TransportPreference {
18    /// Use HTTP/3 over QUIC if the proxy supports it; fall back to TCP.
19    #[default]
20    PreferH3,
21    /// Always use HTTP/1.1 or HTTP/2 over a TCP CONNECT tunnel.
22    ForceTcp,
23    /// Prefer a persistent TCP connection that spans multiple logical requests.
24    ///
25    /// Forces a TCP CONNECT tunnel and signals the HTTP client layer to keep
26    /// the underlying connection open between requests.  Connection lifetime
27    /// and per-connection request limits are governed by
28    /// [`crate::types::ProxyConfig::max_requests_per_connection`] and
29    /// [`crate::types::ProxyConfig::connection_max_age_secs`].
30    PersistentTcp,
31}
32
33/// Resolve the [`RoutingPath`] for a request given proxy capabilities and the
34/// caller's transport preference.
35///
36/// # Decision logic
37///
38/// | Preference       | `supports_http3_tunnel` | Result           |
39/// |------------------|------------------------|------------------|
40/// | `PreferH3`       | `true`                 | `H3OverUdp`      |
41/// | `PreferH3`       | `false`                | `H1H2OverTcp`    |
42/// | `ForceTcp`       | any                    | `H1H2OverTcp`    |
43/// | `PersistentTcp`  | any                    | `PersistentTcp`  |
44///
45/// # Example
46/// ```
47/// use stygian_proxy::routing::{resolve_routing_path, TransportPreference};
48/// use stygian_proxy::types::{ProxyCapabilities, RoutingPath};
49///
50/// let caps = ProxyCapabilities { supports_http3_tunnel: true, ..Default::default() };
51/// assert_eq!(
52///     resolve_routing_path(&caps, TransportPreference::PreferH3),
53///     RoutingPath::H3OverUdp,
54/// );
55/// assert_eq!(
56///     resolve_routing_path(&caps, TransportPreference::ForceTcp),
57///     RoutingPath::H1H2OverTcp,
58/// );
59/// let fallback = ProxyCapabilities::default();
60/// assert_eq!(
61///     resolve_routing_path(&fallback, TransportPreference::PreferH3),
62///     RoutingPath::H1H2OverTcp,
63/// );
64/// ```
65pub const fn resolve_routing_path(
66    capabilities: &ProxyCapabilities,
67    preference: TransportPreference,
68) -> RoutingPath {
69    match preference {
70        TransportPreference::ForceTcp => RoutingPath::H1H2OverTcp,
71        TransportPreference::PersistentTcp => RoutingPath::PersistentTcp,
72        TransportPreference::PreferH3 => {
73            if capabilities.supports_http3_tunnel {
74                RoutingPath::H3OverUdp
75            } else {
76                RoutingPath::H1H2OverTcp
77            }
78        }
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::types::ProxyCapabilities;
86
87    #[test]
88    fn prefer_h3_with_udp_support_returns_h3() {
89        let caps = ProxyCapabilities {
90            supports_http3_tunnel: true,
91            ..Default::default()
92        };
93        assert_eq!(
94            resolve_routing_path(&caps, TransportPreference::PreferH3),
95            RoutingPath::H3OverUdp,
96        );
97    }
98
99    #[test]
100    fn prefer_h3_without_udp_support_falls_back_to_tcp() {
101        let caps = ProxyCapabilities::default();
102        assert_eq!(
103            resolve_routing_path(&caps, TransportPreference::PreferH3),
104            RoutingPath::H1H2OverTcp,
105        );
106    }
107
108    #[test]
109    fn force_tcp_always_returns_tcp() {
110        let caps = ProxyCapabilities {
111            supports_http3_tunnel: true,
112            ..Default::default()
113        };
114        assert_eq!(
115            resolve_routing_path(&caps, TransportPreference::ForceTcp),
116            RoutingPath::H1H2OverTcp,
117        );
118    }
119
120    #[test]
121    fn persistent_tcp_returns_persistent_tcp() {
122        let caps = ProxyCapabilities {
123            supports_http3_tunnel: true,
124            ..Default::default()
125        };
126        assert_eq!(
127            resolve_routing_path(&caps, TransportPreference::PersistentTcp),
128            RoutingPath::PersistentTcp,
129        );
130    }
131}