This commit is contained in:
Mateusz Gruszczyński committed 2026-10-14 14:54:22 +02:00
1 parent 1b9135117c
commit 4f36de240c
32 files changed
+1175 -704

No files matched your search

+41 -5
View File
@@ -22,6 +22,13 @@ use std::{
time::{Duration, Instant},
};
/// Hyper's public error chain omits the GOAWAY reason, but Debug retains
/// the peer-initiated H2 `NO_ERROR` variant. Restrict this to graceful closure.
pub fn is_graceful_h2_goaway(error: &(dyn std::error::Error + 'static)) -> bool {
let chain = format!("{error:?}");
chain.contains("GoAway(") && chain.contains("NO_ERROR") && chain.contains("Remote")
}
static MONOTONIC_START: OnceLock<Instant> = OnceLock::new();
type RawProxyClient = Client<super::TimedConnector, Body>;
@@ -54,6 +61,7 @@ struct RotatingClient {
observed_at_ms: AtomicU64,
observed_h1: AtomicU64,
observed_h2: AtomicU64,
graceful_goaway_at_ms: AtomicU64,
}
/// Backend HTTP client with bounded generation lifetime.
@@ -107,19 +115,43 @@ impl ProxyClient {
observed_at_ms: AtomicU64::new(0),
observed_h1: AtomicU64::new(0),
observed_h2: AtomicU64::new(0),
graceful_goaway_at_ms: AtomicU64::new(0),
}),
}
}
pub async fn request(&self, request: Request<Body>) -> Result<Response<Incoming>, ClientError> {
let client = self.inner.client_for_request();
let generation = self.inner.generation.load(Ordering::Acquire);
let response = client.request(request).await;
if let Ok(value) = &response {
self.inner.observe_protocol(value.version());
match &response {
Ok(value) => self.inner.observe_protocol(value.version()),
Err(error) if is_graceful_h2_goaway(error) => {
// Also applies to cache refresh and other backend HTTP callers,
// not only the regular forward loop.
self.note_graceful_goaway(generation);
}
Err(_) => {}
}
response
}
/// A peer may gracefully close a reused H2 connection. Discard only the
/// affected client generation; concurrent requests must not rotate again.
pub fn note_graceful_goaway(&self, generation: u64) {
self.inner
.graceful_goaway_at_ms
.store(monotonic_millis().saturating_add(1), Ordering::Release);
self.inner.rotate(generation, "h2_goaway_no_error", false);
}
/// A cancellation immediately following GOAWAY may be another stream on
/// the closing H2 connection. Keep this correlation window short.
pub fn recently_saw_graceful_goaway(&self) -> bool {
let at = self.inner.graceful_goaway_at_ms.load(Ordering::Acquire);
at != 0 && monotonic_millis().saturating_add(1).saturating_sub(at) <= 1_000
}
pub fn protocol_stats(&self) -> UpstreamProtocolStats {
self.inner.protocol_stats()
}
@@ -425,14 +457,18 @@ fn build_raw_client(factory: &ClientFactory) -> Result<RawProxyClient> {
url::Host::Ipv6(ip) => format!("[{ip}]"),
host => host.to_string(),
};
let port = origin.port_or_known_default().context("backend port missing")?;
let tls_name = backend.tls_server_name
let port = origin
.port_or_known_default()
.context("backend port missing")?;
let tls_name = backend
.tls_server_name
.as_deref()
.map(crate::config::normalize_host)
.transpose()?
.unwrap_or(network_host);
let destination: http::Uri = format!("{}://{tls_name}:{port}", origin.scheme())
.parse().context("invalid configured backend transport authority")?;
.parse()
.context("invalid configured backend transport authority")?;
Ok(builder.build(TimedConnector {
inner,
policy: policy.clone(),