0.29.7
This commit is contained in:
1 parent
ad95bfb050
commit
1431dd47f0
62 files changed
+1360
-198
No files matched your search
@@ -181,3 +181,29 @@ async fn execute_error_fallback(
|
||||
}
|
||||
Some(response)
|
||||
}
|
||||
|
||||
/// Called only for a response supplied by the Web Server or an HTTP origin.
|
||||
/// Routing/security failures and internal fallback responses are excluded.
|
||||
fn intercept_route_origin_error(
|
||||
response: Response,
|
||||
route: &crate::config::CompiledRoute,
|
||||
snapshot: &crate::config::Snapshot,
|
||||
) -> Response {
|
||||
// An internal named location is the final, administrator-selected fallback
|
||||
// representation. Interception inside its nested forward_request would
|
||||
// otherwise erase its 404 HTML before ErrorFallbackResponse is attached.
|
||||
if route.internal {
|
||||
return response;
|
||||
}
|
||||
let mut response = if let Some(frontend) = route.frontend.as_ref() {
|
||||
crate::pages::intercept_application_error(
|
||||
response,
|
||||
&frontend.pages,
|
||||
&snapshot.envelope.config.pages,
|
||||
)
|
||||
} else {
|
||||
response
|
||||
};
|
||||
response.extensions_mut().insert(crate::pages::OriginProvidedResponse);
|
||||
response
|
||||
}
|
||||
@@ -121,7 +121,7 @@ if let Some(ticket) = &cache_ticket {
|
||||
hit.resident_age_seconds,
|
||||
);
|
||||
hit.response.extensions_mut().insert(ResponseRulesApplied);
|
||||
return hit.response;
|
||||
return intercept_route_origin_error(hit.response, route, &snapshot);
|
||||
}
|
||||
state.cache.record(scope, &host, "MISS", "cache_miss");
|
||||
miss_recorded = true;
|
||||
@@ -188,7 +188,7 @@ let serve_stale = |reason: &'static str, elapsed: Duration, origin_backend: &str
|
||||
exchange.cache_status.store(1, Ordering::Relaxed);
|
||||
*exchange.cache_reason.lock().unwrap_or_else(|e| e.into_inner()) = reason;
|
||||
}
|
||||
Some(hit.response)
|
||||
Some(intercept_route_origin_error(hit.response, route, &snapshot))
|
||||
};
|
||||
if only_cached {
|
||||
let mut response = error(
|
||||
@@ -281,7 +281,7 @@ if handshake.is_none()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = reason;
|
||||
}
|
||||
return hit.response;
|
||||
return intercept_route_origin_error(hit.response, route, &snapshot);
|
||||
}
|
||||
collapse_rounds = collapse_rounds.saturating_add(1);
|
||||
if collapse_rounds >= 8 {
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
).await {
|
||||
return fallback;
|
||||
}
|
||||
// Keep the upstream body as hyper::body::Incoming until the
|
||||
// HTTP/2 WebSocket and regular response paths have adapted it.
|
||||
// The opt-in application error page is applied to Response<Body>
|
||||
// in success_prepare.rs, before transformation/cache admission.
|
||||
let (origin_validation_headers, origin_plan, origin_reason, mut response) =
|
||||
include!("success_prepare.rs");
|
||||
include!("success_finish.rs")
|
||||
|
||||
@@ -87,7 +87,7 @@ if response.status() == StatusCode::NOT_MODIFIED && revalidation_sent {
|
||||
.unwrap_or_else(|e| e.into_inner()) = "cache_revalidated";
|
||||
}
|
||||
state.cache.record(scope, &host, "HIT", "cache_revalidated");
|
||||
return revalidated;
|
||||
return intercept_route_origin_error(revalidated, route, &snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +333,33 @@ let mut response = if h3_tunnel {
|
||||
}
|
||||
Response::from_parts(parts, Body::new(body))
|
||||
};
|
||||
// The upstream HTTP client returns Response<hyper::body::Incoming>. Adapt it
|
||||
// before rendering a RustProxy error page, so HTTP/2 WebSocket tunnels can
|
||||
// continue using the original Incoming body without type erasure.
|
||||
// An internal named-location fallback has already won earlier in response.rs.
|
||||
let intercept_origin_error = !route.internal && route.frontend.as_ref().is_some_and(|frontend| {
|
||||
frontend.pages.intercept_statuses.contains(&response.status().as_u16())
|
||||
});
|
||||
// Mark every upstream response as application-supplied, even if this vhost
|
||||
// does not intercept its status. The observer must not overwrite it merely
|
||||
// because a global 404/5xx error page exists.
|
||||
response = intercept_route_origin_error(response, route, &snapshot);
|
||||
if intercept_origin_error {
|
||||
// The earlier response rules modified the original origin headers;
|
||||
// reapply them to the newly rendered page so the vhost's header policy
|
||||
// remains effective. Do not allow a custom header rule to cache errors.
|
||||
apply_response_rules(
|
||||
response.headers_mut(),
|
||||
&snapshot,
|
||||
route.frontend.as_deref(),
|
||||
Some(route),
|
||||
admin_control_plane,
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
"cache-control",
|
||||
HeaderValue::from_static("no-store"),
|
||||
);
|
||||
}
|
||||
response.extensions_mut().insert(ResponseRulesApplied);
|
||||
response
|
||||
.extensions_mut()
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
}
|
||||
response.extensions_mut().insert(ResponseRulesApplied);
|
||||
response.extensions_mut().insert(StaticSourceResponse);
|
||||
return response;
|
||||
return intercept_route_origin_error(response, route, &snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@
|
||||
// await: the filesystem/PHP source owns the body for its full lifetime.
|
||||
let static_body = std::mem::replace(request.body_mut(), Body::empty());
|
||||
let started = Instant::now();
|
||||
let mut response = crate::web_server::serve(service, request_method.clone(),
|
||||
let response = crate::web_server::serve(service, request_method.clone(),
|
||||
request_headers.clone(), request_uri, &service_path, static_body,
|
||||
crate::web_server::RequestContext {
|
||||
client_ip: context.client_ip,
|
||||
@@ -130,6 +130,7 @@
|
||||
response.status(), &fallback_request, &fallback_body,
|
||||
).await { return fallback; }
|
||||
|
||||
let mut response = intercept_route_origin_error(response, route, &snapshot);
|
||||
let origin_headers = response.headers().clone();
|
||||
let original_plan = crate::cache::plan(response.status(), &origin_headers,
|
||||
cache_settings, started.elapsed());
|
||||
@@ -151,6 +152,10 @@
|
||||
});
|
||||
apply_response_rules(response.headers_mut(), &snapshot, route.frontend.as_deref(),
|
||||
Some(route), false);
|
||||
if response.extensions().get::<crate::pages::PageServed>().is_some() {
|
||||
// A vhost header rule must not make a replaced error cacheable.
|
||||
response.headers_mut().insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
}
|
||||
response.extensions_mut().insert(ResponseRulesApplied);
|
||||
response.extensions_mut().insert(StaticSourceResponse);
|
||||
|
||||
|
||||
@@ -296,7 +296,6 @@ mod authority_tests {
|
||||
"12.00ms"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upstream_uri_preserves_nested_url_query_without_rejecting_valid_separators() {
|
||||
@@ -402,11 +401,64 @@ async fn fallback_terminal_404_html_takes_precedence_over_builtin_error_page() {
|
||||
Bytes::from_static(b"<html><title>ReactPHP 404</title></html>")
|
||||
);
|
||||
|
||||
// A normal response with no fallback is still eligible for the configured
|
||||
// global/vhost error-page replacement.
|
||||
// A proxy-generated response is eligible for configured global/vhost
|
||||
// pages even when interception of *application* errors is disabled.
|
||||
let response = Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
assert!(should_replace_error_page(&response, false));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn application_404_is_not_overwritten_by_global_pages_when_interception_is_off() {
|
||||
use http_body_util::BodyExt;
|
||||
|
||||
for source in ["reverse_proxy", "web_server_php"] {
|
||||
let mut response = Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::from(format!("{source}: origin 404 body")))
|
||||
.unwrap();
|
||||
// Both paths use intercept_route_origin_error to set this marker
|
||||
// once the origin response reaches Response<Body>.
|
||||
response.extensions_mut().insert(crate::pages::OriginProvidedResponse);
|
||||
assert!(!should_replace_error_page(&response, false), "{source}");
|
||||
let bytes = response.into_body().collect().await.unwrap().to_bytes();
|
||||
assert_eq!(bytes.as_ref(), format!("{source}: origin 404 body").as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn compiled_vhost_origin_interception_controls_application_404() {
|
||||
for enabled in [false, true] {
|
||||
let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
config.pages.errors.insert(404, crate::pages::Page {
|
||||
kind: crate::pages::Kind::Html,
|
||||
status: Some(503),
|
||||
html: "RustProxy custom error page".into(),
|
||||
});
|
||||
if enabled {
|
||||
config.vhosts[0].pages.intercept_statuses = vec![404];
|
||||
}
|
||||
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
|
||||
let route = snapshot.select("localhost", "/").unwrap();
|
||||
let response = Response::builder().status(StatusCode::NOT_FOUND)
|
||||
.body(Body::from("application custom 404"))
|
||||
.unwrap();
|
||||
let response = intercept_route_origin_error(response, route, &snapshot);
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
assert!(response.extensions().get::<crate::pages::OriginProvidedResponse>().is_some());
|
||||
assert!(!should_replace_error_page(&response, false));
|
||||
assert_eq!(response.extensions().get::<crate::pages::PageServed>().is_some(), enabled);
|
||||
if enabled {
|
||||
assert_eq!(response.headers().get("cache-control").unwrap(), "no-store");
|
||||
}
|
||||
let bytes = response.into_body().collect().await.unwrap().to_bytes();
|
||||
if enabled {
|
||||
assert_eq!(bytes, "RustProxy custom error page");
|
||||
} else {
|
||||
assert_eq!(bytes, "application custom 404");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ fn request_id_hash(sequence: u64) -> String {
|
||||
fn should_replace_error_page(response: &Response, minimal_welcome: bool) -> bool {
|
||||
!minimal_welcome
|
||||
&& response.extensions().get::<ErrorFallbackResponse>().is_none()
|
||||
&& response.extensions().get::<crate::pages::OriginProvidedResponse>().is_none()
|
||||
&& response.extensions().get::<crate::pages::PageServed>().is_none()
|
||||
&& response
|
||||
.extensions()
|
||||
.get::<crate::tinyauth::AuthenticationFailure>()
|
||||
|
||||
Reference in new issue
Block a user