This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 09:29:10 +02:00
1 parent 2e4aa4df4d
commit 87e571c170
106 files changed
+17952 -6185

No files matched your search

+388 -198
View File
@@ -15,7 +15,7 @@ use axum::{
};
use body::{Direction, Exchange, MeteredBody};
use http::{
header::{HeaderValue, HOST},
header::{HeaderValue, HOST, USER_AGENT},
HeaderMap, Method, StatusCode, Uri, Version,
};
use http_body::Body as HttpBody;
@@ -24,10 +24,16 @@ use http_body_util::BodyExt;
struct ResponseRulesApplied;
#[derive(Clone)]
struct LocalGeneratedResponse;
#[derive(Clone)]
struct StaticSourceResponse;
#[derive(Clone, Copy)]
struct RedirectResponse { http3_enabled: bool }
struct RedirectResponse {
http3_enabled: bool,
}
#[derive(Clone, Copy)]
struct LocalHostResponse { http3_enabled: bool }
struct LocalHostResponse {
http3_enabled: bool,
}
#[derive(Clone, Copy)]
struct OriginTiming(Duration);
#[derive(Clone)]
@@ -80,7 +86,9 @@ fn format_error_chain(error: &(dyn std::error::Error + 'static)) -> String {
parts.join(" -> ")
}
fn classify_stale_error(error: &(dyn std::error::Error + 'static)) -> crate::cache::StaleErrorClass {
fn classify_stale_error(
error: &(dyn std::error::Error + 'static),
) -> crate::cache::StaleErrorClass {
let mut current = Some(error);
let mut text = String::new();
for _ in 0..12 {
@@ -115,7 +123,10 @@ fn classify_stale_error(error: &(dyn std::error::Error + 'static)) -> crate::cac
}
}
fn upstream_error_class(error: &(dyn std::error::Error + 'static), connect_failure: bool) -> &'static str {
fn upstream_error_class(
error: &(dyn std::error::Error + 'static),
connect_failure: bool,
) -> &'static str {
match classify_stale_error(error) {
crate::cache::StaleErrorClass::Dns => "dns",
crate::cache::StaleErrorClass::Tls => "tls",
@@ -217,7 +228,6 @@ where
detail
}
#[derive(Clone)]
struct CacheRefreshContext {
state: Arc<App>,
@@ -271,21 +281,17 @@ async fn refresh_cache_once(
*refresh.version_mut() = Version::HTTP_11;
*refresh.headers_mut() = refresh_headers;
let started = Instant::now();
let Ok(Ok(origin)) = tokio::time::timeout(
context.timeout,
context.backend.client.request(refresh),
)
.await
let Ok(Ok(origin)) =
tokio::time::timeout(context.timeout, context.backend.client.request(refresh)).await
else {
return false;
};
let origin_validation_headers = origin.headers().clone();
let Some(route) = context.snapshot.select_on(
&context.host,
&context.path,
context.local,
context.tls,
) else {
let Some(route) =
context
.snapshot
.select_on(&context.host, &context.path, context.local, context.tls)
else {
return false;
};
if origin.status() == StatusCode::NOT_MODIFIED {
@@ -464,10 +470,8 @@ fn schedule_proactive_refresh(
delay = 5;
continue;
};
let Some(revalidation_guard) = context
.state
.cache
.begin_background_revalidation(&cached)
let Some(revalidation_guard) =
context.state.cache.begin_background_revalidation(&cached)
else {
drop(inflight_guard);
delay = 1;
@@ -547,7 +551,11 @@ fn local_location_response(spec: &crate::vhost::LocationResponse, method: &Metho
let body_allowed = !matches!(status.as_u16(), 100..=199 | 204 | 304) && *method != Method::HEAD;
let mut response = Response::builder()
.status(status)
.body(if body_allowed { Body::from(spec.body.clone()) } else { Body::empty() })
.body(if body_allowed {
Body::from(spec.body.clone())
} else {
Body::empty()
})
.unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, Body::empty()).into_response());
if let Ok(value) = HeaderValue::from_str(&spec.content_type) {
response.headers_mut().insert("content-type", value);
@@ -596,8 +604,7 @@ fn admin_session_passthrough(
tls: bool,
backend: &crate::config::CompiledBackend,
) -> bool {
tls
&& backend.accept_local_listener_risk
tls && backend.accept_local_listener_risk
&& backend.targets_admin_listener
&& snapshot
.envelope
@@ -904,6 +911,17 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
.extensions()
.get::<PeerAddr>()
.map(|peer| peer.0.ip().to_string());
let user_agent = state
.logs
.capture_user_agent()
.then(|| {
request
.headers()
.get(USER_AGENT)
.and_then(|value| value.to_str().ok())
.map(|value| value.chars().take(512).collect::<String>())
})
.flatten();
let (http_version, transport) = match request.version() {
Version::HTTP_10 => ("1.0", "h1"),
Version::HTTP_11 => ("1.1", "h1"),
@@ -939,6 +957,8 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
// the verified effective client address after trusted-proxy/Cloudflare
// processing, so policy and live-log identity remain aligned.
client_ip: Mutex::new(initial_client_ip),
user_agent,
country: Mutex::new(None),
started: Instant::now(),
status: AtomicU16::new(500),
cache_status: AtomicU16::new(0),
@@ -949,6 +969,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
response_bytes: AtomicU64::new(0),
metrics: state.metrics.clone(),
logs: state.logs.clone(),
traffic: state.traffic.clone(),
_permit: permit,
});
if let Ok(value) = HeaderValue::from_str(&id.to_string()) {
@@ -968,7 +989,10 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
))
});
let mut response = next.run(request).await;
let minimal_welcome = response.extensions().get::<crate::pages::MinimalWelcome>().is_some();
let minimal_welcome = response
.extensions()
.get::<crate::pages::MinimalWelcome>()
.is_some();
let code = response.status().as_u16();
let custom_error = frontend_context
.as_ref()
@@ -989,17 +1013,32 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
// minimal. It is never cacheable and carries no optional RustProxy
// diagnostics/server/security/compression/cache-diagnostic headers.
for name in [
"server", "server-timing", "alt-svc", "x-rustproxy-request-id",
"x-rustproxy-revision", "x-rust-proxy-backend",
"x-rust-proxy-backend-time", "x-rustproxy-backend-time", "x-cache", "x-cache-hits",
"x-cache-status", "x-cache-resident-age", "content-encoding", "vary",
"strict-transport-security", "content-security-policy",
"permissions-policy", "referrer-policy", "x-frame-options",
"server",
"server-timing",
"alt-svc",
"x-rustproxy-request-id",
"x-rustproxy-revision",
"x-rust-proxy-backend",
"x-rust-proxy-backend-time",
"x-rustproxy-backend-time",
"x-cache",
"x-cache-hits",
"x-cache-status",
"x-cache-resident-age",
"content-encoding",
"vary",
"strict-transport-security",
"content-security-policy",
"permissions-policy",
"referrer-policy",
"x-frame-options",
"x-content-type-options",
] {
response.headers_mut().remove(name);
}
response.headers_mut().insert("cache-control", HeaderValue::from_static("no-store"));
response
.headers_mut()
.insert("cache-control", HeaderValue::from_static("no-store"));
} else {
if response
.extensions()
@@ -1029,10 +1068,18 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
id,
&state.active_listeners,
);
if response.extensions().get::<RedirectResponse>().is_some_and(|redirect| !redirect.http3_enabled) {
if response
.extensions()
.get::<RedirectResponse>()
.is_some_and(|redirect| !redirect.http3_enabled)
{
response.headers_mut().remove("alt-svc");
}
if response.extensions().get::<LocalHostResponse>().is_some_and(|item| !item.http3_enabled) {
if response
.extensions()
.get::<LocalHostResponse>()
.is_some_and(|item| !item.http3_enabled)
{
response.headers_mut().remove("alt-svc");
}
response = crate::compression::encode(
@@ -1055,8 +1102,18 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
.is_none()
&& response.extensions().get::<RedirectResponse>().is_none()
&& response.extensions().get::<LocalHostResponse>().is_none()
&& response.extensions().get::<crate::pages::PageServed>().is_none()
&& response.extensions().get::<LocalGeneratedResponse>().is_none()
&& response
.extensions()
.get::<crate::pages::PageServed>()
.is_none()
&& response
.extensions()
.get::<LocalGeneratedResponse>()
.is_none()
&& response
.extensions()
.get::<StaticSourceResponse>()
.is_none()
&& response
.extensions()
.get::<crate::tinyauth::AuthenticationFailure>()
@@ -1087,8 +1144,14 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
cache.annotate_transformed(response.headers_mut(), transformed);
}
}
if response.extensions().get::<crate::pages::PageServed>().is_some()
|| response.extensions().get::<LocalGeneratedResponse>().is_some()
if response
.extensions()
.get::<crate::pages::PageServed>()
.is_some()
|| response
.extensions()
.get::<LocalGeneratedResponse>()
.is_some()
{
// Static/placeholder/default pages are generated locally and must never
// look cacheable or carry RustProxy cache diagnostics/validators.
@@ -1145,7 +1208,10 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
}
/// Build a same-host HTTPS redirect while preserving the request path and query.
fn force_ssl_location(host: &str, uri: &Uri, port: u16) -> Option<HeaderValue> {
let path = uri.path_and_query().map(|value| value.as_str()).unwrap_or("/");
let path = uri
.path_and_query()
.map(|value| value.as_str())
.unwrap_or("/");
let location = if port == 443 {
format!("https://{host}{path}")
} else {
@@ -1164,7 +1230,9 @@ fn force_ssl_redirect(
}
let location = force_ssl_location(host, uri, frontend.https_redirect_port?)?;
let mut response = StatusCode::PERMANENT_REDIRECT.into_response();
response.headers_mut().insert(http::header::LOCATION, location);
response
.headers_mut()
.insert(http::header::LOCATION, location);
// Do not pin a redirect in a browser after an operator later disables Force SSL.
response
.headers_mut()
@@ -1295,8 +1363,8 @@ async fn forward(
);
};
let frontend = snapshot.frontend(&host, local.0, tls);
let websocket_legacy_support = handshake.is_some()
&& frontend.is_some_and(|frontend| frontend.websocket_legacy_support);
let websocket_legacy_support =
handshake.is_some() && frontend.is_some_and(|frontend| frontend.websocket_legacy_support);
let redirect = snapshot.redirect_on(&host, &path, local.0, tls);
let local_host = snapshot.not_found_host_on(&host, local.0, tls);
// TLS ALPN normally prevents HTTP/2 from reaching a frontend that disabled it.
@@ -1342,7 +1410,9 @@ async fn forward(
);
}
if !tls {
if let Some(response) = frontend.and_then(|frontend| force_ssl_redirect(frontend, &host, request.uri())) {
if let Some(response) =
frontend.and_then(|frontend| force_ssl_redirect(frontend, &host, request.uri()))
{
return response;
}
}
@@ -1373,10 +1443,24 @@ async fn forward(
}
}
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) = Some(context.client_ip.to_string());
*exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) =
Some(context.client_ip.to_string());
}
if let Some(frontend) = frontend {
let country = state.geoip.country(context.client_ip);
// GeoIP is deliberately off the default request path. Resolve only when
// a country policy/rate-limit requires it or the operator explicitly
// enables country capture for access logs + traffic analytics.
let capture_country = state.logs.capture_country();
let country = (capture_country
|| frontend.profile_country_access.needs_geoip()
|| frontend.rate_limit.needs_geoip())
.then(|| state.geoip.country(context.client_ip))
.flatten();
if capture_country {
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.country.lock().unwrap_or_else(|e| e.into_inner()) = country.clone();
}
}
if !frontend.profile_country_access.allows(country.as_deref())
|| !frontend.rate_limit.country_allowed(country.as_deref())
{
@@ -1424,12 +1508,16 @@ async fn forward(
"RustProxy: redirect target could not be rendered\n",
);
};
response.extensions_mut().insert(RedirectResponse { http3_enabled: _redirect.http3_enabled });
response.extensions_mut().insert(RedirectResponse {
http3_enabled: _redirect.http3_enabled,
});
return response;
}
if let Some(item) = local_host {
let mut response = item.page.serve(request.method(), StatusCode::NOT_FOUND);
response.extensions_mut().insert(LocalHostResponse { http3_enabled: item.http3_enabled });
response.extensions_mut().insert(LocalHostResponse {
http3_enabled: item.http3_enabled,
});
return response;
}
let selected = snapshot.select_on(&host, &path, local.0, tls);
@@ -1537,12 +1625,69 @@ async fn forward(
if let Some(spec) = &route.response {
return local_location_response(spec, request.method());
}
if let crate::config::CompiledSource::WebServer(service) = &route.source {
if handshake.is_some() {
return error(
StatusCode::BAD_REQUEST,
"RustProxy: Web Server source does not support WebSocket upgrades\n",
);
}
let service_path = match route.rewritten_path(&path) {
Ok(Some(path)) => path,
Ok(None) => path.clone(),
Err(_) => {
return error(
StatusCode::BAD_REQUEST,
"RustProxy: invalid Web Server rewrite path\n",
)
}
};
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone());
*exchange
.cache_effective_enabled
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some(false);
*exchange
.transform_effective_enabled
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some(false);
}
// Do not hold a shared reference to Request<Body> across await. Axum handlers
// must return Send futures, while Body is not Sync. Clone only static-request
// metadata needed by the filesystem source before entering the async server.
let static_method = request.method().clone();
let static_headers = request.headers().clone();
let static_uri = request.uri().clone();
let static_version = request.version();
let static_body = std::mem::replace(request.body_mut(), Body::empty());
let mut response = crate::web_server::serve(
service,
static_method,
static_headers,
static_uri,
&service_path,
static_body,
crate::web_server::RequestContext {
client_ip: context.client_ip,
client_port: Some(peer.0.port()),
server_name: host.clone(),
server_port: local.0.port(),
tls,
version: static_version,
},
)
.await;
response.extensions_mut().insert(StaticSourceResponse);
return response;
}
let (backend, backend_lease, affinity_cookie) = if let Some(pool) = &route.pool {
let selection = match &route.backend_id {
Some(id) => pool.select_member_wait(id).await,
None => pool
.select_wait(context.client_ip, request.headers(), tls)
.await,
None => {
pool.select_wait(context.client_ip, request.headers(), tls)
.await
}
};
let selection = match selection {
Ok(selection) => selection,
@@ -1573,7 +1718,13 @@ async fn forward(
selection.cookie,
)
} else {
(route.backend.clone(), None, None)
let Some(backend) = route.backend.clone() else {
return error(
StatusCode::INTERNAL_SERVER_ERROR,
"RustProxy: Reverse Proxy route has no backend\n",
);
};
(backend, None, None)
};
let admin_control_plane = admin_session_passthrough(&snapshot, &host, tls, &backend);
if admin_control_plane {
@@ -1588,9 +1739,14 @@ async fn forward(
let scope = route.frontend.as_ref().map(|f| f.id.as_str()).unwrap_or("");
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone());
*exchange.cache_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) =
Some(cache_settings.active_for(route.cache));
*exchange.transform_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) =
*exchange
.cache_effective_enabled
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some(cache_settings.active_for(route.cache));
*exchange
.transform_effective_enabled
.lock()
.unwrap_or_else(|e| e.into_inner()) =
Some(transform_settings.is_some_and(|settings| settings.enabled));
}
let policy_reason = if admin_control_plane {
@@ -1654,7 +1810,10 @@ async fn forward(
Ok(None) => public_suffix.clone(),
Ok(Some(rewritten)) => {
let Some(mut encoded) = encoded_origin_path(&rewritten) else {
return error(StatusCode::BAD_REQUEST, "RustProxy: invalid rewritten request target\n");
return error(
StatusCode::BAD_REQUEST,
"RustProxy: invalid rewritten request target\n",
);
};
if let Some(query) = request.uri().query() {
encoded.push('?');
@@ -1662,7 +1821,12 @@ async fn forward(
}
encoded
}
Err(_) => return error(StatusCode::BAD_REQUEST, "RustProxy: invalid rewritten request target\n"),
Err(_) => {
return error(
StatusCode::BAD_REQUEST,
"RustProxy: invalid rewritten request target\n",
)
}
};
let Some(uri) = upstream_uri(&backend, &origin_suffix) else {
return error(
@@ -1684,8 +1848,7 @@ async fn forward(
None
};
let mut h2_downstream = if handshake.as_ref().is_some_and(|handshake| handshake.h2())
&& (backend.protocol != crate::config::UpstreamProtocol::Http2
|| websocket_legacy_support)
&& (backend.protocol != crate::config::UpstreamProtocol::Http2 || websocket_legacy_support)
{
Some(std::mem::replace(request.body_mut(), Body::empty()))
} else {
@@ -1847,7 +2010,9 @@ async fn forward(
path: path.clone(),
scope: scope.to_owned(),
timeout: frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
.and_then(|frontend| {
frontend.proxy_timeouts.response_header_timeout()
})
.unwrap_or(state.header_timeout),
local: local.0,
tls,
@@ -1867,10 +2032,7 @@ async fn forward(
});
}
}
if cache_settings.proactive_refresh_enabled
&& !hit.stale
&& hit.remaining_ttl > 0
{
if cache_settings.proactive_refresh_enabled && !hit.stale && hit.remaining_ttl > 0 {
schedule_proactive_refresh(
CacheRefreshContext {
state: state.clone(),
@@ -1884,7 +2046,9 @@ async fn forward(
path: path.clone(),
scope: scope.to_owned(),
timeout: frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
.and_then(|frontend| {
frontend.proxy_timeouts.response_header_timeout()
})
.unwrap_or(state.header_timeout),
local: local.0,
tls,
@@ -1918,10 +2082,8 @@ async fn forward(
if hit.transformed {
state.transform_metrics.record_cache_hit(scope);
}
cache_settings.annotate_resident_age(
hit.response.headers_mut(),
hit.resident_age_seconds,
);
cache_settings
.annotate_resident_age(hit.response.headers_mut(), hit.resident_age_seconds);
hit.response.extensions_mut().insert(ResponseRulesApplied);
return hit.response;
}
@@ -1959,39 +2121,35 @@ async fn forward(
.unwrap_or_else(|e| e.into_inner()) = reason;
}
}
let serve_stale = |reason: &'static str, elapsed: Duration, origin_backend: &str| -> Option<Response> {
let cached = cached_revalidation.as_ref()?;
let mut hit = state.cache.stale_if_error_hit(cached)?;
if let crate::cache::RangeSelection::Single(range) = range_selection {
hit = hit.apply_range(range, if_range.as_ref());
}
cache_settings.annotate_reason(
hit.response.headers_mut(),
"HIT",
hit.hits,
0,
reason,
);
cache_settings.annotate_transformed(hit.response.headers_mut(), hit.transformed);
if hit.transformed {
state.transform_metrics.record_cache_hit(scope);
}
cache_settings.annotate_resident_age(
hit.response.headers_mut(),
hit.resident_age_seconds,
);
hit.response.extensions_mut().insert(ResponseRulesApplied);
hit.response.extensions_mut().insert(OriginTiming(elapsed));
hit.response
.extensions_mut()
.insert(OriginBackend(origin_backend.to_owned()));
state.cache.record(scope, &host, "HIT", reason);
if let Some(exchange) = &exchange {
exchange.cache_status.store(1, Ordering::Relaxed);
*exchange.cache_reason.lock().unwrap_or_else(|e| e.into_inner()) = reason;
}
Some(hit.response)
};
let serve_stale =
|reason: &'static str, elapsed: Duration, origin_backend: &str| -> Option<Response> {
let cached = cached_revalidation.as_ref()?;
let mut hit = state.cache.stale_if_error_hit(cached)?;
if let crate::cache::RangeSelection::Single(range) = range_selection {
hit = hit.apply_range(range, if_range.as_ref());
}
cache_settings.annotate_reason(hit.response.headers_mut(), "HIT", hit.hits, 0, reason);
cache_settings.annotate_transformed(hit.response.headers_mut(), hit.transformed);
if hit.transformed {
state.transform_metrics.record_cache_hit(scope);
}
cache_settings
.annotate_resident_age(hit.response.headers_mut(), hit.resident_age_seconds);
hit.response.extensions_mut().insert(ResponseRulesApplied);
hit.response.extensions_mut().insert(OriginTiming(elapsed));
hit.response
.extensions_mut()
.insert(OriginBackend(origin_backend.to_owned()));
state.cache.record(scope, &host, "HIT", reason);
if let Some(exchange) = &exchange {
exchange.cache_status.store(1, Ordering::Relaxed);
*exchange
.cache_reason
.lock()
.unwrap_or_else(|e| e.into_inner()) = reason;
}
Some(hit.response)
};
if only_cached {
let mut response = error(
StatusCode::GATEWAY_TIMEOUT,
@@ -2006,9 +2164,7 @@ async fn forward(
// A follower can wake after a different Vary representation was published; in
// that case it competes for the next flight instead of stampeding the origin.
let mut fill_leader = None;
if handshake.is_none()
&& matches!(range_selection, crate::cache::RangeSelection::None)
{
if handshake.is_none() && matches!(range_selection, crate::cache::RangeSelection::None) {
if let Some(ticket) = cache_ticket.as_ref() {
let mut collapse_rounds = 0u8;
loop {
@@ -2018,7 +2174,8 @@ async fn forward(
break;
}
crate::cache::FillClaim::Waited => {
if let Some(mut hit) = state.cache.get_tiered(ticket, cache_settings).await {
if let Some(mut hit) = state.cache.get_tiered(ticket, cache_settings).await
{
let conditional = crate::cache::conditional_not_modified(
&client_conditionals,
hit.response.headers(),
@@ -2045,7 +2202,9 @@ async fn forward(
path: path.clone(),
scope: scope.to_owned(),
timeout: frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
.and_then(|frontend| {
frontend.proxy_timeouts.response_header_timeout()
})
.unwrap_or(state.header_timeout),
local: local.0,
tls,
@@ -2087,11 +2246,9 @@ async fn forward(
}
collapse_rounds = collapse_rounds.saturating_add(1);
if collapse_rounds >= 8 {
state.cache.record_reason(
scope,
&host,
"cache_collapse_variant_limit",
);
state
.cache
.record_reason(scope, &host, "cache_collapse_variant_limit");
break;
}
}
@@ -2100,13 +2257,17 @@ async fn forward(
cache_settings.collapse_timeout_fallback,
crate::cache::CollapseTimeoutFallback::StaleIfAvailable
) {
if let Some(stale) =
serve_stale("cache_collapse_timeout_stale", Duration::ZERO, &backend.id)
{
if let Some(stale) = serve_stale(
"cache_collapse_timeout_stale",
Duration::ZERO,
&backend.id,
) {
return stale;
}
}
state.cache.record_reason(scope, &host, "cache_collapse_timeout_origin");
state
.cache
.record_reason(scope, &host, "cache_collapse_timeout_origin");
break;
}
crate::cache::FillClaim::Disabled => break,
@@ -2250,7 +2411,9 @@ async fn forward(
&& handshake.is_none()
&& request_body_empty
&& retry_safe_method(&retry_method)
&& retry_policy.as_ref().is_some_and(|policy| policy.max_retries > 0);
&& retry_policy
.as_ref()
.is_some_and(|policy| policy.max_retries > 0);
let upstream_started = Instant::now();
let mut attempt_started = Instant::now();
let mut active_backend = backend.clone();
@@ -2269,7 +2432,11 @@ async fn forward(
};
let result = tokio::time::timeout(
response_header_timeout,
client.request(request_attempt.take().expect("upstream request attempt missing")),
client.request(
request_attempt
.take()
.expect("upstream request attempt missing"),
),
)
.await;
let attempt_elapsed = attempt_started.elapsed();
@@ -2292,12 +2459,7 @@ async fn forward(
break result;
};
let selection = match pool
.select_retry_wait(
context.client_ip,
&retry_headers,
tls,
&attempted_backends,
)
.select_retry_wait(context.client_ip, &retry_headers, tls, &attempted_backends)
.await
{
Ok(selection) => selection,
@@ -2472,8 +2634,7 @@ async fn forward(
};
if handshake.as_ref().is_some_and(|handshake| {
handshake.h3() || (handshake.h2() && !websocket_upstream_h2)
})
&& response.status().is_success()
}) && response.status().is_success()
&& response.status() != StatusCode::SWITCHING_PROTOCOLS
{
if let Some(exchange) = &exchange {
@@ -2535,8 +2696,8 @@ async fn forward(
if let Some(cookie) = active_affinity_cookie {
parts.headers.append("set-cookie", cookie);
}
let h3_tunnel = handshake.as_ref().is_some_and(|handshake| handshake.h3())
&& upgraded.is_some();
let h3_tunnel =
handshake.as_ref().is_some_and(|handshake| handshake.h3()) && upgraded.is_some();
let h2_bridge_tunnel = handshake.as_ref().is_some_and(|handshake| handshake.h2())
&& !websocket_upstream_h2
&& upgraded.is_some();
@@ -2589,15 +2750,13 @@ async fn forward(
parts.headers.remove("content-length");
parts.headers.remove("transfer-encoding");
parts.extensions.insert(crate::compression::Skip);
parts
.extensions
.insert(websocket::H3Tunnel::new(
upstream,
exchange,
active_backend.origin.clone(),
lease,
stop,
));
parts.extensions.insert(websocket::H3Tunnel::new(
upstream,
exchange,
active_backend.origin.clone(),
lease,
stop,
));
Response::from_parts(parts, Body::empty())
} else if h2_bridge_tunnel {
let Some(upstream) = upgraded.take() else {
@@ -2716,41 +2875,41 @@ async fn forward(
.insert(OriginBackend(active_backend.id.clone()));
if !range_requested && handshake.is_none() {
if let Some(settings) = transform_settings.filter(|settings| settings.enabled) {
let transform_started = Instant::now();
response = match crate::transform::apply(
response,
&public_suffix,
settings,
origin_no_transform,
)
.await
{
Ok(response) => {
state.transform_metrics.record_response(
scope,
&response,
transform_started.elapsed(),
);
response
}
Err(cause) => {
state
.transform_metrics
.record_error(scope, transform_started.elapsed());
state.cache.record_reason(scope, &host, "transform_error");
if let Some(exchange) = &exchange {
*exchange
.cache_reason
.lock()
.unwrap_or_else(|e| e.into_inner()) = "transform_error";
let transform_started = Instant::now();
response = match crate::transform::apply(
response,
&public_suffix,
settings,
origin_no_transform,
)
.await
{
Ok(response) => {
state.transform_metrics.record_response(
scope,
&response,
transform_started.elapsed(),
);
response
}
tracing::debug!(error = %cause, "static response transformation failed");
return error(
StatusCode::BAD_GATEWAY,
"RustProxy: upstream response body could not be transformed\n",
);
}
};
Err(cause) => {
state
.transform_metrics
.record_error(scope, transform_started.elapsed());
state.cache.record_reason(scope, &host, "transform_error");
if let Some(exchange) = &exchange {
*exchange
.cache_reason
.lock()
.unwrap_or_else(|e| e.into_inner()) = "transform_error";
}
tracing::debug!(error = %cause, "static response transformation failed");
return error(
StatusCode::BAD_GATEWAY,
"RustProxy: upstream response body could not be transformed\n",
);
}
};
}
}
@@ -2927,7 +3086,9 @@ async fn forward(
path: path.clone(),
scope: scope.to_owned(),
timeout: frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
.and_then(|frontend| {
frontend.proxy_timeouts.response_header_timeout()
})
.unwrap_or(state.header_timeout),
local: local.0,
tls,
@@ -2959,7 +3120,10 @@ async fn forward(
let detail = upstream_failure_detail(&cause, &active_backend, elapsed, connect_failure);
if let Some(exchange) = &exchange {
exchange.websocket_error("upstream_request_failed", "upstream_http_error");
*exchange.upstream_error.lock().unwrap_or_else(|e| e.into_inner()) = Some(detail.clone());
*exchange
.upstream_error
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some(detail.clone());
}
if let Some(lease) = &active_lease {
lease.error_detail(
@@ -2994,7 +3158,9 @@ async fn forward(
};
let stale_class = classify_stale_error(&cause);
if cache_settings.allows_stale_error_class(stale_class) {
if let Some(stale) = serve_stale("cache_stale_if_error", elapsed, &active_backend.id) {
if let Some(stale) =
serve_stale("cache_stale_if_error", elapsed, &active_backend.id)
{
return stale;
}
}
@@ -3022,7 +3188,10 @@ async fn forward(
}
if let Some(exchange) = &exchange {
exchange.websocket_error("upstream_timeout", "header_timeout");
*exchange.upstream_error.lock().unwrap_or_else(|e| e.into_inner()) = Some("response_header_timeout".into());
*exchange
.upstream_error
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some("response_header_timeout".into());
}
if let Some(lease) = &active_lease {
lease.error_detail(
@@ -3031,12 +3200,12 @@ async fn forward(
final_attempt_elapsed,
);
}
if cache_settings
.allows_stale_error_class(crate::cache::StaleErrorClass::Timeout)
{
if let Some(stale) =
serve_stale("cache_stale_if_error", upstream_started.elapsed(), &active_backend.id)
{
if cache_settings.allows_stale_error_class(crate::cache::StaleErrorClass::Timeout) {
if let Some(stale) = serve_stale(
"cache_stale_if_error",
upstream_started.elapsed(),
&active_backend.id,
) {
return stale;
}
}
@@ -3078,7 +3247,7 @@ mod authority_tests {
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
let route = snapshot.select("localhost", "/").unwrap();
assert_eq!(
upstream_request_version(&route.backend, false),
upstream_request_version(route.backend.as_ref().unwrap(), false),
Version::HTTP_2
);
}
@@ -3107,9 +3276,8 @@ mod authority_tests {
#[tokio::test]
async fn h3_delete_unknown_nonempty_body_is_replayed_not_buffered_away() {
use bytes::Bytes;
let stream = futures_util::stream::iter([Ok::<_, std::io::Error>(
Bytes::from_static(b"payload"),
)]);
let stream =
futures_util::stream::iter([Ok::<_, std::io::Error>(Bytes::from_static(b"payload"))]);
let mut request = Request::builder()
.method(Method::DELETE)
.version(Version::HTTP_3)
@@ -3221,7 +3389,10 @@ mod authority_tests {
for name in ["etag", "x-cache", "x-cache-reason", "cache-status"] {
assert!(!response.headers().contains_key(name), "{name}");
}
assert!(response.extensions().get::<LocalGeneratedResponse>().is_some());
assert!(response
.extensions()
.get::<LocalGeneratedResponse>()
.is_some());
}
#[test]
fn http2_uses_authority_and_rejects_conflicting_host() {
@@ -3238,11 +3409,9 @@ mod authority_tests {
}
#[test]
fn legacy_websocket_origin_matches_upstream_scheme_and_host() {
let origin = legacy_websocket_origin(
"https://ilo.internal:443",
&"10.0.0.5:443".parse().unwrap(),
)
.unwrap();
let origin =
legacy_websocket_origin("https://ilo.internal:443", &"10.0.0.5:443".parse().unwrap())
.unwrap();
assert_eq!(origin, "https://10.0.0.5:443");
assert!(legacy_websocket_origin("not-an-origin", &"10.0.0.5".parse().unwrap()).is_none());
}
@@ -3311,18 +3480,39 @@ mod authority_tests {
#[test]
fn backend_diagnostics_are_opt_in_and_strip_spoofed_values() {
let mut response = Response::new(Body::empty());
response.headers_mut().insert("x-rust-proxy-backend", HeaderValue::from_static("spoofed"));
response.headers_mut().insert("x-rust-proxy-backend-time", HeaderValue::from_static("legacy-spoof"));
response.headers_mut().insert("x-rustproxy-backend-time", HeaderValue::from_static("999ms"));
response
.headers_mut()
.insert("x-rust-proxy-backend", HeaderValue::from_static("spoofed"));
response.headers_mut().insert(
"x-rust-proxy-backend-time",
HeaderValue::from_static("legacy-spoof"),
);
response.headers_mut().insert(
"x-rustproxy-backend-time",
HeaderValue::from_static("999ms"),
);
apply_backend_diagnostic_values(&mut response, false, false);
assert!(response.headers().get("x-rust-proxy-backend").is_none());
assert!(response.headers().get("x-rust-proxy-backend-time").is_none());
assert!(response
.headers()
.get("x-rust-proxy-backend-time")
.is_none());
assert!(response.headers().get("x-rustproxy-backend-time").is_none());
response.extensions_mut().insert(OriginBackend("origin-a".into()));
response.extensions_mut().insert(OriginTiming(Duration::from_millis(12)));
response
.extensions_mut()
.insert(OriginBackend("origin-a".into()));
response
.extensions_mut()
.insert(OriginTiming(Duration::from_millis(12)));
apply_backend_diagnostic_values(&mut response, true, true);
assert_eq!(response.headers().get("x-rust-proxy-backend").unwrap(), "origin-a");
assert_eq!(response.headers().get("x-rustproxy-backend-time").unwrap(), "12.00ms");
assert_eq!(
response.headers().get("x-rust-proxy-backend").unwrap(),
"origin-a"
);
assert_eq!(
response.headers().get("x-rustproxy-backend-time").unwrap(),
"12.00ms"
);
}
}