v0.34.0_1

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-17 20:34:13 +02:00
1 parent 9b3d6731cc
commit 00f29f92c8
68 files changed
+2599 -513

No files matched your search

+10 -11
View File
@@ -4,16 +4,16 @@
// allow-only country entries and explicit IP allows form one policy scope.
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone());
*exchange.source.lock().unwrap_or_else(|e| e.into_inner()) = if route.response.is_some() {
"local_response"
exchange.set_source(if route.response.is_some() {
ExchangeSource::LocalResponse
} else if route.redirect.is_some() {
"local_redirect"
ExchangeSource::LocalRedirect
} else {
match &route.source {
crate::config::CompiledSource::WebServer(_) => "web_server",
crate::config::CompiledSource::ReverseProxy => "reverse_proxy",
crate::config::CompiledSource::WebServer(_) => ExchangeSource::WebServer,
crate::config::CompiledSource::ReverseProxy => ExchangeSource::ReverseProxy,
}
};
});
}
// Local location responses intentionally still pass host/profile/location ACLs
// and the outer response-header/security pipeline, but never select/open an upstream.
@@ -126,10 +126,8 @@ let transform_settings = route.frontend.as_ref().map(|f| &f.transform);
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()) =
Some(transform_settings.is_some_and(|settings| settings.enabled));
exchange.set_cache_enabled(cache_settings.active_for(route.cache));
exchange.set_transform_enabled(transform_settings.is_some_and(|settings| settings.enabled));
}
let policy_reason = if admin_control_plane {
Some("admin_control_plane")
@@ -304,7 +302,8 @@ let key_headers = cache_key_headers
.as_ref()
.unwrap_or_else(|| request.headers());
let cache_ticket = if effective_cacheable {
state.cache.ticket_scoped(
state.cache.ticket_scoped_for_config(
snapshot.envelope.revision,
state.cache.revision(),
route.frontend.as_ref().map(|f| f.id.as_str()).unwrap_or(""),
context.scheme,
+32 -15
View File
@@ -60,15 +60,36 @@ if pending_revalidation {
if request.method() == Method::HEAD && cache_ticket.is_some() {
*request.method_mut() = Method::GET;
}
// Preserve a pre-normalization Host snapshot for pool retries. Each next
// backend gets its OWN target and Host/:authority, never the previous peer's.
let retry_headers = request.headers().clone();
// Save the pre-normalization headers only for replayable requests that may
// actually be retried. The common one-backend HTTP/1 route has no retry peer
// or H2 GOAWAY handling, so copying HeaderMap for every request was wasted.
// Never replay writes, streaming uploads, auth or WebSocket requests.
let retry_method = request.method().clone();
let retry_policy = route.pool.as_ref().map(|pool| &pool.settings.retry);
let pool_retry_possible = route.backend_id.is_none()
&& !admin_control_plane
&& handshake.is_none()
&& retry_safe_method(&retry_method)
&& route.pool.as_ref().is_some_and(|pool| pool.members.len() > 1)
&& retry_policy.is_some_and(|policy| policy.max_retries > 0);
let h2_replay_possible = !admin_control_plane
&& handshake.is_none()
&& retry_safe_method(&retry_method)
&& matches!(backend.protocol,
crate::config::UpstreamProtocol::Http2
| crate::config::UpstreamProtocol::H2
| crate::config::UpstreamProtocol::Auto);
let replay_headers = ((pool_retry_possible || h2_replay_possible)
&& (request.body().size_hint().exact() == Some(0)
|| request.version() == Version::HTTP_3))
.then(|| request.headers().clone());
let expected_backend_host = if backend.preserve_host {
&original_host
} else {
&backend.host_header
};
let retry_host_follows_backend = retry_headers.get(HOST)
let retry_host_follows_backend = replay_headers.as_ref()
.and_then(|headers| headers.get(HOST))
.is_some_and(|value| value == expected_backend_host);
let Some(upstream_target) = upstream_request_target(
&uri, request.headers_mut(), &backend,
@@ -125,14 +146,7 @@ let proactive_base_headers = if cache_ticket.is_some() && cache_settings.proacti
let response_header_timeout = frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
.unwrap_or(state.header_timeout);
let retry_method = request.method().clone();
let retry_policy = route.pool.as_ref().map(|pool| pool.settings.retry.clone());
let retry_enabled = route.backend_id.is_none()
&& !admin_control_plane
&& handshake.is_none()
&& request_body_empty
&& retry_safe_method(&retry_method)
&& retry_policy.as_ref().is_some_and(|policy| policy.max_retries > 0);
let retry_enabled = pool_retry_possible && request_body_empty && replay_headers.is_some();
let upstream_started = Instant::now();
let mut attempt_started = Instant::now();
let mut active_backend = backend.clone();
@@ -165,15 +179,15 @@ let upstream_result = loop {
&& (is_graceful_h2_goaway(cause)
|| is_related_h2_close(cause, &active_backend))
{
if let Some(next_request) = retry_request(
if let Some(next_request) = replay_headers.as_ref().and_then(|headers| retry_request(
&retry_method,
upstream_request_version(&active_backend, false),
&retry_headers,
headers,
&active_backend,
&original_host,
&origin_suffix,
retry_host_follows_backend,
) {
)) {
graceful_retry_done = true;
if let Some(lease) = &active_lease {
lease.retry_attempted();
@@ -201,6 +215,9 @@ let upstream_result = loop {
break result;
}
attempted_backends.push(active_backend.id.clone());
let Some(retry_headers) = replay_headers.as_ref() else {
break result;
};
let Some(pool) = route.pool.as_ref() else {
break result;
};
+39 -4
View File
@@ -11,6 +11,14 @@
Err(_) => return error(StatusCode::BAD_REQUEST,
"RustProxy: invalid Web Server rewrite path\n"),
};
#[cfg(target_os = "linux")]
let kernel_sendfile = request.extensions()
.get::<crate::server::sendfile::SendfileSlots>().cloned();
#[cfg(target_os = "linux")]
let identity_request = request.version() == http::Version::HTTP_11
&& !request.headers().contains_key("accept-encoding")
&& !request.headers().contains_key("range")
&& !request.headers().contains_key("if-range");
let cache_settings = route.frontend.as_ref()
.map(|frontend| &frontend.cache)
.unwrap_or(&snapshot.envelope.config.cache);
@@ -19,9 +27,8 @@
let enabled = cache_settings.active_for(route.cache)
&& frontend.is_none_or(|frontend| frontend.basic_auth.is_none() && frontend.tinyauth.is_none());
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.cache_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) = Some(enabled);
*exchange.transform_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) =
Some(transform_settings.is_some_and(|settings| settings.enabled));
exchange.set_cache_enabled(enabled);
exchange.set_transform_enabled(transform_settings.is_some_and(|settings| settings.enabled));
}
// The authority is already validated by effective_host. HTTP/2 and HTTP/3
// requests commonly have :authority but no Host field, which otherwise
@@ -64,7 +71,8 @@
Some(headers)
} else { None };
let cache_ticket = if eligible {
state.cache.ticket_scoped(state.cache.revision(), scope, context.scheme,
state.cache.ticket_scoped_for_config(snapshot.envelope.revision,
state.cache.revision(), scope, context.scheme,
original_host.to_str().unwrap_or(""), &route.id, &public_suffix,
key_headers.as_ref().unwrap_or(&request_headers))
} else { None };
@@ -280,5 +288,32 @@
cache_settings.annotate_reason(response.headers_mut(), outcome, 0, 0, reason);
let transformed = response.extensions().get::<crate::transform::Applied>().is_some();
cache_settings.annotate_transformed(response.headers_mut(), transformed);
// Register only AFTER the vhost response policy, cache and transformation
// decisions are final. Never feed synthetic transport placeholders into
// cache, compression or the transformation engine. The ordinary body
// remains the fallback on every other protocol/response and when the
// per-connection bridge has no capacity.
#[cfg(target_os = "linux")]
if !enabled && identity_request && request_method == Method::GET
&& !transform_settings.is_some_and(|settings| settings.enabled)
&& response.status() == StatusCode::OK
&& !response.headers().contains_key("content-encoding")
&& !response.headers().contains_key("transfer-encoding")
&& !response.headers().contains_key("content-range")
{
if let (Some(slots), Some(candidate)) = (
kernel_sendfile,
response.extensions().get::<crate::server::sendfile::SendfileCandidate>().cloned(),
) {
let correct_length = response.headers().get("content-length")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok()) == Some(candidate.length);
if correct_length {
if let Some(body) = slots.create_body(candidate, service.web_metrics.clone()) {
*response.body_mut() = Body::new(body);
}
}
}
}
response
}