v0.34.0_1
This commit is contained in:
1 parent
9b3d6731cc
commit
00f29f92c8
68 files changed
+2599
-513
No files matched your search
+71
-20
@@ -10,7 +10,7 @@ use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU16, AtomicU64, Ordering},
|
||||
atomic::{AtomicBool, AtomicU16, AtomicU64, AtomicU8, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
task::{Context, Poll},
|
||||
@@ -21,13 +21,44 @@ use tokio::{
|
||||
time::{sleep_until, Sleep},
|
||||
};
|
||||
|
||||
/// Compact source tags avoid a mutex on every completed exchange.
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ExchangeSource {
|
||||
Unresolved = 0,
|
||||
LocalResponse = 1,
|
||||
LocalRedirect = 2,
|
||||
WebServer = 3,
|
||||
ReverseProxy = 4,
|
||||
}
|
||||
impl ExchangeSource {
|
||||
fn name(value: u8) -> &'static str {
|
||||
match value {
|
||||
1 => "local_response",
|
||||
2 => "local_redirect",
|
||||
3 => "web_server",
|
||||
4 => "reverse_proxy",
|
||||
_ => "unresolved",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 0 = unknown, 1 = Some(false), 2 = Some(true). Preserve log schema.
|
||||
fn read_effective(value: u8) -> Option<bool> {
|
||||
match value {
|
||||
1 => Some(false),
|
||||
2 => Some(true),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Exchange {
|
||||
pub vhost_metrics: Option<Arc<crate::metrics::VhostMetrics>>,
|
||||
pub vhost: Option<String>,
|
||||
pub route: Mutex<Option<String>>,
|
||||
pub source: Mutex<&'static str>,
|
||||
pub cache_effective_enabled: Mutex<Option<bool>>,
|
||||
pub transform_effective_enabled: Mutex<Option<bool>>,
|
||||
pub source: AtomicU8,
|
||||
pub cache_effective_enabled: AtomicU8,
|
||||
pub transform_effective_enabled: AtomicU8,
|
||||
pub backend: Mutex<Option<Arc<crate::balance::Lease>>>,
|
||||
pub id: u64,
|
||||
pub request_id: String,
|
||||
@@ -59,6 +90,17 @@ pub struct Exchange {
|
||||
pub _permit: OwnedSemaphorePermit,
|
||||
}
|
||||
impl Exchange {
|
||||
pub fn set_source(&self, source: ExchangeSource) {
|
||||
self.source.store(source as u8, Ordering::Relaxed);
|
||||
}
|
||||
pub fn set_cache_enabled(&self, enabled: bool) {
|
||||
self.cache_effective_enabled
|
||||
.store(if enabled { 2 } else { 1 }, Ordering::Relaxed);
|
||||
}
|
||||
pub fn set_transform_enabled(&self, enabled: bool) {
|
||||
self.transform_effective_enabled
|
||||
.store(if enabled { 2 } else { 1 }, Ordering::Relaxed);
|
||||
}
|
||||
pub fn websocket_phase(&self, phase: &'static str) {
|
||||
if self.websocket_bootstrap.is_some() {
|
||||
*self
|
||||
@@ -145,15 +187,9 @@ impl Drop for Exchange {
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let route = self.route.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
let source = *self.source.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let cache_effective_enabled = *self
|
||||
.cache_effective_enabled
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let transform_effective_enabled = *self
|
||||
.transform_effective_enabled
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let source = ExchangeSource::name(self.source.load(Ordering::Relaxed));
|
||||
let cache_effective_enabled = read_effective(self.cache_effective_enabled.load(Ordering::Relaxed));
|
||||
let transform_effective_enabled = read_effective(self.transform_effective_enabled.load(Ordering::Relaxed));
|
||||
let client_ip = self
|
||||
.client_ip
|
||||
.lock()
|
||||
@@ -213,17 +249,17 @@ impl Drop for Exchange {
|
||||
authority: None,
|
||||
matched_by: None,
|
||||
cache_reason: *self.cache_reason.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
vhost: self.vhost.clone(),
|
||||
vhost: self.vhost.take(),
|
||||
route,
|
||||
cache_effective_enabled,
|
||||
transform_effective_enabled,
|
||||
backend,
|
||||
id: self.id,
|
||||
request_id: self.request_id.clone(),
|
||||
request_id: std::mem::take(&mut self.request_id),
|
||||
time_ms: now_ms(),
|
||||
method: self.method.clone(),
|
||||
host: self.host.clone(),
|
||||
path: self.path.clone(),
|
||||
method: std::mem::take(&mut self.method),
|
||||
host: std::mem::take(&mut self.host),
|
||||
path: std::mem::take(&mut self.path),
|
||||
http_version: self.http_version,
|
||||
transport: self.transport,
|
||||
websocket_bootstrap: self.websocket_bootstrap,
|
||||
@@ -243,7 +279,7 @@ impl Drop for Exchange {
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone(),
|
||||
client_ip,
|
||||
user_agent: self.user_agent.clone(),
|
||||
user_agent: self.user_agent.take(),
|
||||
country,
|
||||
status,
|
||||
duration_ms,
|
||||
@@ -262,6 +298,9 @@ pub enum Direction {
|
||||
pub struct MeteredBody {
|
||||
inner: Body,
|
||||
exchange: Option<Arc<Exchange>>,
|
||||
/// For a response, backend selection is final before this wrapper is made.
|
||||
/// Keep the selected lease alive and avoid locking Exchange.backend per frame.
|
||||
response_backend: Option<Arc<crate::balance::Lease>>,
|
||||
direction: Direction,
|
||||
timer: Option<Pin<Box<Sleep>>>,
|
||||
deadline: tokio::time::Instant,
|
||||
@@ -271,9 +310,13 @@ pub struct MeteredBody {
|
||||
impl MeteredBody {
|
||||
pub fn new(inner: Body, exchange: Arc<Exchange>, direction: Direction, idle: Duration) -> Self {
|
||||
let finished = inner.is_end_stream();
|
||||
let response_backend = matches!(direction, Direction::Response)
|
||||
.then(|| exchange.backend.lock().unwrap_or_else(|e| e.into_inner()).clone())
|
||||
.flatten();
|
||||
Self {
|
||||
inner,
|
||||
exchange: Some(exchange),
|
||||
response_backend,
|
||||
direction,
|
||||
// Ready/empty bodies never need a timer allocation. Keep the
|
||||
// original deadline so a delayed first poll still times out.
|
||||
@@ -286,6 +329,7 @@ impl MeteredBody {
|
||||
fn finish(&mut self) {
|
||||
self.finished = true;
|
||||
self.timer.take();
|
||||
self.response_backend.take();
|
||||
self.exchange.take();
|
||||
}
|
||||
}
|
||||
@@ -305,7 +349,14 @@ impl HttpBody for MeteredBody {
|
||||
Poll::Ready(Some(Ok(frame))) => {
|
||||
if let (Some(data), Some(exchange)) = (frame.data_ref(), this.exchange.as_ref()) {
|
||||
let count = data.len() as u64;
|
||||
exchange.backend_bytes(count, matches!(this.direction, Direction::Request));
|
||||
match this.direction {
|
||||
Direction::Request => exchange.backend_bytes(count, true),
|
||||
Direction::Response => {
|
||||
if let Some(lease) = &this.response_backend {
|
||||
lease.bytes(count, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
match this.direction {
|
||||
Direction::Request => {
|
||||
exchange.request_bytes.fetch_add(count, Ordering::Relaxed);
|
||||
|
||||
+27
-3
@@ -11,9 +11,9 @@ mod tests {
|
||||
vhost_metrics: None,
|
||||
vhost: None,
|
||||
route: Mutex::new(None),
|
||||
source: Mutex::new("unresolved"),
|
||||
cache_effective_enabled: Mutex::new(None),
|
||||
transform_effective_enabled: Mutex::new(None),
|
||||
source: AtomicU8::new(ExchangeSource::Unresolved as u8),
|
||||
cache_effective_enabled: AtomicU8::new(0),
|
||||
transform_effective_enabled: AtomicU8::new(0),
|
||||
backend: Mutex::new(None),
|
||||
id: 1,
|
||||
request_id: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
|
||||
@@ -225,4 +225,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn atomic_exchange_tags_keep_original_observability_fields() {
|
||||
let (exchange, slots) = observation().await;
|
||||
let logs = exchange.logs.clone();
|
||||
exchange.set_source(ExchangeSource::ReverseProxy);
|
||||
exchange.set_cache_enabled(false);
|
||||
exchange.set_transform_enabled(true);
|
||||
drop(exchange);
|
||||
assert_eq!(slots.available_permits(), 1);
|
||||
let entries = logs.recent();
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].cache_effective_enabled, Some(false));
|
||||
assert_eq!(entries[0].transform_effective_enabled, Some(true));
|
||||
assert_eq!(entries[0].cache_reason, "not_proxy_response");
|
||||
assert_eq!(entries[0].method, "POST");
|
||||
assert_eq!(entries[0].host, "localhost");
|
||||
assert_eq!(entries[0].path, "/echo");
|
||||
assert_eq!(entries[0].request_id.len(), 64);
|
||||
assert_eq!(ExchangeSource::name(ExchangeSource::ReverseProxy as u8), "reverse_proxy");
|
||||
assert_eq!(read_effective(0), None);
|
||||
assert_eq!(read_effective(1), Some(false));
|
||||
assert_eq!(read_effective(2), Some(true));
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ fn response_context(
|
||||
request: &Request,
|
||||
) -> (
|
||||
Option<Arc<crate::config::CompiledFrontend>>,
|
||||
Option<crate::config::CompiledRoute>,
|
||||
Option<Arc<crate::config::CompiledRoute>>,
|
||||
) {
|
||||
let tls = request
|
||||
.extensions()
|
||||
@@ -174,7 +174,7 @@ fn response_context(
|
||||
let frontend = snapshot.frontend(&host, local, tls).cloned();
|
||||
let route = crate::acl::routing_path(request.uri().path())
|
||||
.ok()
|
||||
.and_then(|path| snapshot.select_on(&host, &path, local, tls).cloned());
|
||||
.and_then(|path| snapshot.select_on_arc(&host, &path, local, tls).cloned());
|
||||
Some((frontend, route))
|
||||
})();
|
||||
context.unwrap_or((None, None))
|
||||
|
||||
@@ -6,7 +6,7 @@ struct InternalFallbackRoute {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FinalRouteContext(crate::config::CompiledRoute);
|
||||
struct FinalRouteContext(Arc<crate::config::CompiledRoute>);
|
||||
|
||||
/// The named location has already produced the terminal HTTP response.
|
||||
/// In particular, do not replace its 404 HTML with a global/vhost error page.
|
||||
@@ -135,20 +135,21 @@ async fn execute_error_fallback(
|
||||
tls: bool,
|
||||
route: &crate::config::CompiledRoute,
|
||||
status: StatusCode,
|
||||
template: &ErrorFallbackRequest,
|
||||
template: &Option<ErrorFallbackRequest>,
|
||||
body: &ErrorFallbackBody,
|
||||
) -> Option<Response> {
|
||||
let fallback = route
|
||||
.error_fallback
|
||||
.as_ref()
|
||||
.filter(|fallback| fallback.matches(status))?;
|
||||
let template = template.as_ref()?;
|
||||
let frontend = route.frontend.as_ref()?;
|
||||
if template.depth >= MAX_ERROR_FALLBACK_DEPTH {
|
||||
tracing::warn!(vhost = %frontend.id, route = %route.id, target = %fallback.target, "error fallback depth limit reached");
|
||||
return None;
|
||||
}
|
||||
let Some(target) = snapshot
|
||||
.select_internal_on(host, &frontend.id, &fallback.target, local.0, tls)
|
||||
.select_internal_on_arc(host, &frontend.id, &fallback.target, local.0, tls)
|
||||
.cloned()
|
||||
else {
|
||||
tracing::warn!(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -107,7 +107,14 @@ async fn forward_request(
|
||||
"RustProxy: ambiguous request path\n",
|
||||
);
|
||||
};
|
||||
let frontend = snapshot.frontend(&host, local.0, tls);
|
||||
let selected_context = request.extensions().get::<SelectedRouteContext>().cloned();
|
||||
let frontend = if let Some(selection) = &selected_context {
|
||||
selection.frontend.as_ref()
|
||||
} else {
|
||||
// Internal error fallback calls forward_request directly, without
|
||||
// rerunning the observation middleware.
|
||||
snapshot.frontend(&host, local.0, tls)
|
||||
};
|
||||
if snapshot.user_agent_blocking.blocks(request.headers())
|
||||
|| frontend.is_some_and(|frontend| frontend.user_agent_blocking.blocks(request.headers()))
|
||||
{
|
||||
@@ -307,6 +314,8 @@ async fn forward_request(
|
||||
} else {
|
||||
snapshot.select_internal_on(&host, &dispatch.vhost_id, &dispatch.target, local.0, tls)
|
||||
}
|
||||
} else if let Some(selection) = &selected_context {
|
||||
selection.route.as_deref()
|
||||
} else {
|
||||
snapshot.select_on(&host, &path, local.0, tls)
|
||||
};
|
||||
@@ -343,7 +352,11 @@ async fn forward_request(
|
||||
);
|
||||
}
|
||||
}
|
||||
let fallback_request = ErrorFallbackRequest::capture(&request, &snapshot);
|
||||
// Normal routes almost never have an internal error fallback. Do not copy
|
||||
// the complete HeaderMap and request metadata when no fallback exists.
|
||||
let fallback_request = selected
|
||||
.filter(|route| route.error_fallback.is_some())
|
||||
.map(|_| ErrorFallbackRequest::capture(&request, &snapshot));
|
||||
// The admin cookie is never ordinary application state. Strip it BEFORE
|
||||
// auth/cache and keep only the removed fragment locally. A verified self-admin
|
||||
// backend may restore it after the actual upstream has been selected.
|
||||
|
||||
+9
-2
@@ -13,7 +13,7 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
Extension, Router,
|
||||
};
|
||||
use body::{Direction, Exchange, MeteredBody};
|
||||
use body::{Direction, Exchange, ExchangeSource, MeteredBody};
|
||||
use bytes::Bytes;
|
||||
use http::{
|
||||
header::{HeaderValue, HOST, USER_AGENT},
|
||||
@@ -21,6 +21,13 @@ use http::{
|
||||
};
|
||||
use http_body::Body as HttpBody;
|
||||
use http_body_util::BodyExt;
|
||||
/// A route resolved against the immutable snapshot in the observation layer.
|
||||
/// The internal error-fallback dispatcher must resolve its target independently.
|
||||
#[derive(Clone)]
|
||||
struct SelectedRouteContext {
|
||||
frontend: Option<Arc<crate::config::CompiledFrontend>>,
|
||||
route: Option<Arc<crate::config::CompiledRoute>>,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
struct ResponseRulesApplied;
|
||||
#[derive(Clone)]
|
||||
@@ -42,7 +49,7 @@ struct OriginBackend(String);
|
||||
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU16, AtomicU64, Ordering},
|
||||
atomic::{AtomicBool, AtomicU16, AtomicU64, AtomicU8, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
|
||||
@@ -23,6 +23,12 @@ fn should_replace_error_page(response: &Response, minimal_welcome: bool) -> bool
|
||||
async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next) -> Response {
|
||||
let snapshot = state.config.load_full();
|
||||
let (frontend_context, route_context) = response_context(&snapshot, &request);
|
||||
// The same snapshot is inserted into the request. No second host/regex
|
||||
// selection or expensive full CompiledRoute clone is necessary in forward().
|
||||
request.extensions_mut().insert(SelectedRouteContext {
|
||||
frontend: frontend_context.clone(),
|
||||
route: route_context.clone(),
|
||||
});
|
||||
let local = request.extensions().get::<LocalAddr>().map(|v| v.0);
|
||||
let transport_tls = request
|
||||
.extensions()
|
||||
@@ -90,7 +96,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
response.headers_mut(),
|
||||
&snapshot,
|
||||
frontend_context.as_deref(),
|
||||
route_context.as_ref(),
|
||||
route_context.as_deref(),
|
||||
false,
|
||||
);
|
||||
finish_headers(
|
||||
@@ -106,7 +112,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
response.headers_mut(),
|
||||
&snapshot,
|
||||
frontend_context.as_deref(),
|
||||
route_context.as_ref(),
|
||||
route_context.as_deref(),
|
||||
);
|
||||
return response;
|
||||
};
|
||||
@@ -153,9 +159,9 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
vhost_metrics: frontend_context.as_ref().map(|f| f.metrics.clone()),
|
||||
vhost: frontend_context.as_ref().map(|f| f.id.clone()),
|
||||
route: Mutex::new(None),
|
||||
source: Mutex::new("unresolved"),
|
||||
cache_effective_enabled: Mutex::new(None),
|
||||
transform_effective_enabled: Mutex::new(None),
|
||||
source: AtomicU8::new(ExchangeSource::Unresolved as u8),
|
||||
cache_effective_enabled: AtomicU8::new(0),
|
||||
transform_effective_enabled: AtomicU8::new(0),
|
||||
backend: Mutex::new(None),
|
||||
id,
|
||||
request_id: request_id.clone(),
|
||||
@@ -210,7 +216,9 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
.extensions()
|
||||
.get::<FinalRouteContext>()
|
||||
.map(|context| context.0.clone());
|
||||
let effective_route_context = final_route_context.as_ref().or(route_context.as_ref());
|
||||
let effective_route_context = final_route_context
|
||||
.as_deref()
|
||||
.or_else(|| route_context.as_deref());
|
||||
let minimal_welcome = response
|
||||
.extensions()
|
||||
.get::<crate::pages::MinimalWelcome>()
|
||||
|
||||
Reference in new issue
Block a user