|
|
|
|
@@ -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);
|
|
|
|
|
|
|
|
|
|
|