fix(rustproxy-http): cache backend Alt-Svc only from original upstream responses during protocol auto-detection

This commit is contained in:
2026-03-19 21:24:05 +00:00
parent 4cf13c36f8
commit d70c2d77ed
3 changed files with 35 additions and 23 deletions

View File

@@ -1,5 +1,12 @@
# Changelog
## 2026-03-19 - 25.16.2 - fix(rustproxy-http)
cache backend Alt-Svc only from original upstream responses during protocol auto-detection
- Moves Alt-Svc discovery into streaming response construction so it reads backend headers before response filters inject client-facing Alt-Svc values
- Stores the protocol cache key in connection activity during auto-detect mode and clears it after HTTP/3 connection failure to avoid re-caching failed H3 routes
- Prevents fallback requests from reintroducing stale or self-injected Alt-Svc entries that could cause repeated H3 retry loops
## 2026-03-19 - 25.16.1 - fix(http-proxy)
avoid repeated HTTP/3 recaching after QUIC fallback and document backend protocol selection

View File

@@ -43,6 +43,10 @@ struct ConnActivity {
/// increments on creation and decrements on Drop, keeping the watchdog aware that
/// a response body is still streaming after the request handler has returned.
active_requests: Option<Arc<AtomicU64>>,
/// Protocol cache key for Alt-Svc discovery. When set, `build_streaming_response`
/// checks the backend's original response headers for Alt-Svc before our
/// ResponseFilter injects its own. None when not in auto-detect mode or after H3 failure.
alt_svc_cache_key: Option<crate::protocol_cache::ProtocolCacheKey>,
}
/// Default upstream connect timeout (30 seconds).
@@ -341,7 +345,7 @@ impl HttpProxyService {
let cn = cancel_inner.clone();
let la = Arc::clone(&la_inner);
let st = start;
let ca = ConnActivity { last_activity: Arc::clone(&la_inner), start, active_requests: Some(Arc::clone(&ar_inner)) };
let ca = ConnActivity { last_activity: Arc::clone(&la_inner), start, active_requests: Some(Arc::clone(&ar_inner)), alt_svc_cache_key: None };
async move {
let result = svc.handle_request(req, peer, port, cn, ca).await;
// Mark request end — update activity timestamp before guard drops
@@ -418,7 +422,7 @@ impl HttpProxyService {
peer_addr: std::net::SocketAddr,
port: u16,
cancel: CancellationToken,
conn_activity: ConnActivity,
mut conn_activity: ConnActivity,
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
let host = req.headers()
.get("host")
@@ -703,9 +707,11 @@ impl HttpProxyService {
ProtocolDecision::AlpnProbe => (false, true),
};
// Track whether H3 connect failed — suppresses Alt-Svc re-caching to prevent
// the loop: H3 cached → QUIC timeout → H2/H1 fallback → Alt-Svc re-caches H3 → repeat
let mut h3_connect_failed = false;
// Set Alt-Svc cache key on conn_activity so build_streaming_response can check
// the backend's original Alt-Svc header before ResponseFilter injects our own.
if is_auto_detect_mode {
conn_activity.alt_svc_cache_key = Some(protocol_cache_key.clone());
}
// --- H3 path: try QUIC connection before TCP ---
if let ProtocolDecision::H3 { port: h3_port } = protocol_decision {
@@ -742,7 +748,9 @@ impl HttpProxyService {
Err(e) => {
warn!(backend = %upstream_key, error = %e,
"H3 backend connect failed, falling back to H2/H1");
h3_connect_failed = true;
// Suppress Alt-Svc caching for the fallback to prevent re-caching H3
// from our own injected Alt-Svc header or a stale backend Alt-Svc
conn_activity.alt_svc_cache_key = None;
// Force ALPN probe on TCP fallback so we correctly detect H2 vs H1
// (don't cache anything yet — let the ALPN probe decide)
if is_auto_detect_mode && upstream.use_tls {
@@ -948,22 +956,6 @@ impl HttpProxyService {
self.upstream_selector.connection_ended(&upstream_key);
self.metrics.backend_connection_closed(&upstream_key);
// --- Alt-Svc discovery: check if backend advertises H3 ---
// Suppress Alt-Svc caching when we just failed an H3 attempt to prevent the loop:
// H3 cached → QUIC timeout → fallback → Alt-Svc re-caches H3 → repeat.
// The ALPN probe already cached H1 or H2; it will expire after 5min TTL,
// at which point we'll re-probe and see Alt-Svc again, retrying QUIC then.
if is_auto_detect_mode && !h3_connect_failed {
if let Ok(ref resp) = result {
if let Some(alt_svc) = resp.headers().get("alt-svc").and_then(|v| v.to_str().ok()) {
if let Some(h3_port) = parse_alt_svc_h3_port(alt_svc) {
debug!(backend = %upstream_key, h3_port, "Backend advertises H3 via Alt-Svc");
self.protocol_cache.insert_h3(protocol_cache_key, h3_port);
}
}
}
}
result
}
@@ -1762,6 +1754,19 @@ impl HttpProxyService {
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
let (resp_parts, resp_body) = upstream_response.into_parts();
// Check for Alt-Svc in the backend's ORIGINAL response headers BEFORE
// ResponseFilter::apply_headers runs — the filter may inject our own Alt-Svc
// for client-facing HTTP/3 advertisement, which must not be confused with
// backend-originated Alt-Svc.
if let Some(ref cache_key) = conn_activity.alt_svc_cache_key {
if let Some(alt_svc) = resp_parts.headers.get("alt-svc").and_then(|v| v.to_str().ok()) {
if let Some(h3_port) = parse_alt_svc_h3_port(alt_svc) {
debug!(h3_port, "Backend advertises H3 via Alt-Svc");
self.protocol_cache.insert_h3(cache_key.clone(), h3_port);
}
}
}
let mut response = Response::builder()
.status(resp_parts.status);

View File

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