fix(protocol-cache): Include requested_host in protocol detection cache key to avoid cache oscillation when multiple frontend domains share the same backend

This commit is contained in:
2026-03-08 15:24:18 +00:00
parent 4f1c5c919f
commit d4aa46aed7
4 changed files with 23 additions and 5 deletions

View File

@@ -1,5 +1,13 @@
# Changelog # 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<String> 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) ## 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 Cancel connections for routes removed/disabled by adding per-route cancellation tokens and make RouteManager swappable (ArcSwap) for runtime updates

View File

@@ -1,7 +1,8 @@
//! Bounded, TTL-based protocol detection cache for HTTP/2 auto-detection. //! Bounded, TTL-based protocol detection cache for HTTP/2 auto-detection.
//! //!
//! Caches the ALPN-negotiated protocol (H1 or H2) per backend endpoint (host:port). //! Caches the ALPN-negotiated protocol (H1 or H2) per backend endpoint and requested
//! Prevents repeated ALPN probes for backends whose protocol is already known. //! 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::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -27,11 +28,14 @@ pub enum DetectedProtocol {
H2, H2,
} }
/// Key for the protocol cache: (host, port). /// Key for the protocol cache: (host, port, requested_host).
#[derive(Clone, Debug, Hash, Eq, PartialEq)] #[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct ProtocolCacheKey { pub struct ProtocolCacheKey {
pub host: String, pub host: String,
pub port: u16, 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<String>,
} }
/// A cached protocol detection result with a timestamp. /// A cached protocol detection result with a timestamp.
@@ -73,7 +77,7 @@ impl ProtocolCache {
pub fn get(&self, key: &ProtocolCacheKey) -> Option<DetectedProtocol> { pub fn get(&self, key: &ProtocolCacheKey) -> Option<DetectedProtocol> {
let entry = self.cache.get(key)?; let entry = self.cache.get(key)?;
if entry.detected_at.elapsed() < PROTOCOL_CACHE_TTL { 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) Some(entry.protocol)
} else { } else {
// Expired — remove and return None to trigger re-probe // Expired — remove and return None to trigger re-probe

View File

@@ -593,6 +593,7 @@ impl HttpProxyService {
let cache_key = crate::protocol_cache::ProtocolCacheKey { let cache_key = crate::protocol_cache::ProtocolCacheKey {
host: upstream.host.clone(), host: upstream.host.clone(),
port: upstream.port, port: upstream.port,
requested_host: host.clone(),
}; };
match self.protocol_cache.get(&cache_key) { match self.protocol_cache.get(&cache_key) {
Some(crate::protocol_cache::DetectedProtocol::H2) => (true, false), Some(crate::protocol_cache::DetectedProtocol::H2) => (true, false),
@@ -650,6 +651,7 @@ impl HttpProxyService {
let cache_key = crate::protocol_cache::ProtocolCacheKey { let cache_key = crate::protocol_cache::ProtocolCacheKey {
host: upstream.host.clone(), host: upstream.host.clone(),
port: upstream.port, port: upstream.port,
requested_host: host.clone(),
}; };
let detected = if is_h2 { let detected = if is_h2 {
crate::protocol_cache::DetectedProtocol::H2 crate::protocol_cache::DetectedProtocol::H2
@@ -721,6 +723,7 @@ impl HttpProxyService {
self.forward_h2_with_fallback( self.forward_h2_with_fallback(
io, parts, body, upstream_headers, &upstream_path, io, parts, body, upstream_headers, &upstream_path,
&upstream, route_match.route, route_id, &ip_str, &final_pool_key, &upstream, route_match.route, route_id, &ip_str, &final_pool_key,
host.clone(),
).await ).await
} else { } else {
// Explicit H2 mode: hard-fail on handshake error (preserved behavior) // Explicit H2 mode: hard-fail on handshake error (preserved behavior)
@@ -907,6 +910,7 @@ impl HttpProxyService {
route_id: Option<&str>, route_id: Option<&str>,
source_ip: &str, source_ip: &str,
pool_key: &crate::connection_pool::PoolKey, pool_key: &crate::connection_pool::PoolKey,
requested_host: Option<String>,
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> { ) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
let exec = hyper_util::rt::TokioExecutor::new(); let exec = hyper_util::rt::TokioExecutor::new();
let handshake_result: Result<( let handshake_result: Result<(
@@ -961,6 +965,7 @@ impl HttpProxyService {
let cache_key = crate::protocol_cache::ProtocolCacheKey { let cache_key = crate::protocol_cache::ProtocolCacheKey {
host: upstream.host.clone(), host: upstream.host.clone(),
port: upstream.port, port: upstream.port,
requested_host: requested_host.clone(),
}; };
self.protocol_cache.insert(cache_key, crate::protocol_cache::DetectedProtocol::H1); self.protocol_cache.insert(cache_key, crate::protocol_cache::DetectedProtocol::H1);
Ok(error_response(StatusCode::BAD_GATEWAY, "Backend protocol mismatch, retrying with 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 { let cache_key = crate::protocol_cache::ProtocolCacheKey {
host: upstream.host.clone(), host: upstream.host.clone(),
port: upstream.port, port: upstream.port,
requested_host: requested_host.clone(),
}; };
self.protocol_cache.insert(cache_key, crate::protocol_cache::DetectedProtocol::H1); self.protocol_cache.insert(cache_key, crate::protocol_cache::DetectedProtocol::H1);

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartproxy', 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.' 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.'
} }