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}
24
25/// Resolve the [`RoutingPath`] for a request given proxy capabilities and the
26/// caller's transport preference.
27///
28/// # Decision logic
29///
30/// | Preference   | `supports_http3_tunnel` | Result          |
31/// |--------------|------------------------|-----------------|
32/// | `PreferH3`   | `true`                 | `H3OverUdp`    |
33/// | `PreferH3`   | `false`                | `H1H2OverTcp`  |
34/// | `ForceTcp`   | any                    | `H1H2OverTcp`  |
35///
36/// # Example
37/// ```
38/// use stygian_proxy::routing::{resolve_routing_path, TransportPreference};
39/// use stygian_proxy::types::{ProxyCapabilities, RoutingPath};
40///
41/// let caps = ProxyCapabilities { supports_http3_tunnel: true, ..Default::default() };
42/// assert_eq!(
43///     resolve_routing_path(&caps, TransportPreference::PreferH3),
44///     RoutingPath::H3OverUdp,
45/// );
46/// assert_eq!(
47///     resolve_routing_path(&caps, TransportPreference::ForceTcp),
48///     RoutingPath::H1H2OverTcp,
49/// );
50/// let fallback = ProxyCapabilities::default();
51/// assert_eq!(
52///     resolve_routing_path(&fallback, TransportPreference::PreferH3),
53///     RoutingPath::H1H2OverTcp,
54/// );
55/// ```
56pub const fn resolve_routing_path(
57    capabilities: &ProxyCapabilities,
58    preference: TransportPreference,
59) -> RoutingPath {
60    match preference {
61        TransportPreference::ForceTcp => RoutingPath::H1H2OverTcp,
62        TransportPreference::PreferH3 => {
63            if capabilities.supports_http3_tunnel {
64                RoutingPath::H3OverUdp
65            } else {
66                RoutingPath::H1H2OverTcp
67            }
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::types::ProxyCapabilities;
76
77    #[test]
78    fn prefer_h3_with_udp_support_returns_h3() {
79        let caps = ProxyCapabilities {
80            supports_http3_tunnel: true,
81            ..Default::default()
82        };
83        assert_eq!(
84            resolve_routing_path(&caps, TransportPreference::PreferH3),
85            RoutingPath::H3OverUdp,
86        );
87    }
88
89    #[test]
90    fn prefer_h3_without_udp_support_falls_back_to_tcp() {
91        let caps = ProxyCapabilities::default();
92        assert_eq!(
93            resolve_routing_path(&caps, TransportPreference::PreferH3),
94            RoutingPath::H1H2OverTcp,
95        );
96    }
97
98    #[test]
99    fn force_tcp_always_returns_tcp() {
100        let caps = ProxyCapabilities {
101            supports_http3_tunnel: true,
102            ..Default::default()
103        };
104        assert_eq!(
105            resolve_routing_path(&caps, TransportPreference::ForceTcp),
106            RoutingPath::H1H2OverTcp,
107        );
108    }
109}