diff --git a/changelog.md b/changelog.md index 4ddb77d..8888416 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-03-08 - 25.9.2 - fix(protocol-cache) +Include requested_host in protocol detection cache key to avoid cache oscillation when multiple frontend domains share the same backend + +- Add ProtocolCacheKey.requested_host: Option to distinguish cache entries by incoming request Host/:authority +- Update protocol cache lookups/inserts in proxy_service to populate requested_host +- Enhance debug logging to show requested_host on cache hits +- Fixes repeated ALPN probing / cache oscillation when different frontend domains share a backend with differing HTTP/2 support + ## 2026-03-03 - 25.9.1 - fix(rustproxy) Cancel connections for routes removed/disabled by adding per-route cancellation tokens and make RouteManager swappable (ArcSwap) for runtime updates diff --git a/rust/crates/rustproxy-http/src/protocol_cache.rs b/rust/crates/rustproxy-http/src/protocol_cache.rs index ad1b4eb..8c9c5a4 100644 --- a/rust/crates/rustproxy-http/src/protocol_cache.rs +++ b/rust/crates/rustproxy-http/src/protocol_cache.rs @@ -1,7 +1,8 @@ //! Bounded, TTL-based protocol detection cache for HTTP/2 auto-detection. //! -//! Caches the ALPN-negotiated protocol (H1 or H2) per backend endpoint (host:port). -//! Prevents repeated ALPN probes for backends whose protocol is already known. +//! Caches the ALPN-negotiated protocol (H1 or H2) per backend endpoint and requested +//! domain (host:port + requested_host). This prevents cache oscillation when multiple +//! frontend domains share the same backend but differ in HTTP/2 support. use std::sync::Arc; use std::time::{Duration, Instant}; @@ -27,11 +28,14 @@ pub enum DetectedProtocol { H2, } -/// Key for the protocol cache: (host, port). +/// Key for the protocol cache: (host, port, requested_host). #[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct ProtocolCacheKey { pub host: String, pub port: u16, + /// The incoming request's domain (Host header / :authority). + /// Distinguishes protocol detection when multiple domains share the same backend. + pub requested_host: Option, } /// A cached protocol detection result with a timestamp. @@ -73,7 +77,7 @@ impl ProtocolCache { pub fn get(&self, key: &ProtocolCacheKey) -> Option { let entry = self.cache.get(key)?; if entry.detected_at.elapsed() < PROTOCOL_CACHE_TTL { - debug!("Protocol cache hit: {:?} for {}:{}", entry.protocol, key.host, key.port); + debug!("Protocol cache hit: {:?} for {}:{} (requested: {:?})", entry.protocol, key.host, key.port, key.requested_host); Some(entry.protocol) } else { // Expired — remove and return None to trigger re-probe diff --git a/rust/crates/rustproxy-http/src/proxy_service.rs b/rust/crates/rustproxy-http/src/proxy_service.rs index dd88d6b..e633cc3 100644 --- a/rust/crates/rustproxy-http/src/proxy_service.rs +++ b/rust/crates/rustproxy-http/src/proxy_service.rs @@ -593,6 +593,7 @@ impl HttpProxyService { let cache_key = crate::protocol_cache::ProtocolCacheKey { host: upstream.host.clone(), port: upstream.port, + requested_host: host.clone(), }; match self.protocol_cache.get(&cache_key) { Some(crate::protocol_cache::DetectedProtocol::H2) => (true, false), @@ -650,6 +651,7 @@ impl HttpProxyService { let cache_key = crate::protocol_cache::ProtocolCacheKey { host: upstream.host.clone(), port: upstream.port, + requested_host: host.clone(), }; let detected = if is_h2 { crate::protocol_cache::DetectedProtocol::H2 @@ -721,6 +723,7 @@ impl HttpProxyService { self.forward_h2_with_fallback( io, parts, body, upstream_headers, &upstream_path, &upstream, route_match.route, route_id, &ip_str, &final_pool_key, + host.clone(), ).await } else { // Explicit H2 mode: hard-fail on handshake error (preserved behavior) @@ -907,6 +910,7 @@ impl HttpProxyService { route_id: Option<&str>, source_ip: &str, pool_key: &crate::connection_pool::PoolKey, + requested_host: Option, ) -> Result>, hyper::Error> { let exec = hyper_util::rt::TokioExecutor::new(); let handshake_result: Result<( @@ -961,6 +965,7 @@ impl HttpProxyService { let cache_key = crate::protocol_cache::ProtocolCacheKey { host: upstream.host.clone(), port: upstream.port, + requested_host: requested_host.clone(), }; self.protocol_cache.insert(cache_key, crate::protocol_cache::DetectedProtocol::H1); Ok(error_response(StatusCode::BAD_GATEWAY, "Backend protocol mismatch, retrying with H1")) @@ -979,6 +984,7 @@ impl HttpProxyService { let cache_key = crate::protocol_cache::ProtocolCacheKey { host: upstream.host.clone(), port: upstream.port, + requested_host: requested_host.clone(), }; self.protocol_cache.insert(cache_key, crate::protocol_cache::DetectedProtocol::H1); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 9ff41d9..4bb252c 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartproxy', - version: '25.9.1', + version: '25.9.2', description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.' }