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/// ```
65#[must_use]
66pub const fn resolve_routing_path(
67    capabilities: &ProxyCapabilities,
68    preference: TransportPreference,
69) -> RoutingPath {
70    match preference {
71        TransportPreference::ForceTcp => RoutingPath::H1H2OverTcp,
72        TransportPreference::PersistentTcp => RoutingPath::PersistentTcp,
73        TransportPreference::PreferH3 => {
74            if capabilities.supports_http3_tunnel {
75                RoutingPath::H3OverUdp
76            } else {
77                RoutingPath::H1H2OverTcp
78            }
79        }
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::types::ProxyCapabilities;
87
88    #[test]
89    fn prefer_h3_with_udp_support_returns_h3() {
90        let caps = ProxyCapabilities {
91            supports_http3_tunnel: true,
92            ..Default::default()
93        };
94        assert_eq!(
95            resolve_routing_path(&caps, TransportPreference::PreferH3),
96            RoutingPath::H3OverUdp,
97        );
98    }
99
100    #[test]
101    fn prefer_h3_without_udp_support_falls_back_to_tcp() {
102        let caps = ProxyCapabilities::default();
103        assert_eq!(
104            resolve_routing_path(&caps, TransportPreference::PreferH3),
105            RoutingPath::H1H2OverTcp,
106        );
107    }
108
109    #[test]
110    fn force_tcp_always_returns_tcp() {
111        let caps = ProxyCapabilities {
112            supports_http3_tunnel: true,
113            ..Default::default()
114        };
115        assert_eq!(
116            resolve_routing_path(&caps, TransportPreference::ForceTcp),
117            RoutingPath::H1H2OverTcp,
118        );
119    }
120
121    #[test]
122    fn persistent_tcp_returns_persistent_tcp() {
123        let caps = ProxyCapabilities {
124            supports_http3_tunnel: true,
125            ..Default::default()
126        };
127        assert_eq!(
128            resolve_routing_path(&caps, TransportPreference::PersistentTcp),
129            RoutingPath::PersistentTcp,
130        );
131    }
132}