This commit is contained in:
Mateusz Gruszczyński committed 2026-10-25 09:58:39 +01:00
1 parent 37c197c095
commit 5ac3617947
48 files changed
+1049 -255

No files matched your search

+4
View File
@@ -87,6 +87,10 @@ fn enforce_local_page_no_cache(response: &mut Response) {
"x-cache-status",
"x-cache-resident-age",
"x-cache-reason",
"x-upstream-cache",
"x-upstream-cache-hits",
"x-upstream-cache-reason",
"x-upstream-cache-resident-age",
"cache-status",
] {
response.headers_mut().remove(name);
+144 -1
View File
@@ -472,7 +472,9 @@ mod cache_fast_path_tests {
"cache-control",
HeaderValue::from_static(match parts.uri.path() {
"/private" => "private, max-age=600",
"/backend-no-cache" => "public, no-cache, max-age=600",
"/backend-no-cache" | "/cache-headers-revalidate" => {
"public, no-cache, max-age=600"
}
"/backend-zero-stale" => "public, max-age=0, stale-if-error=120",
"/grace" => "public, max-age=1",
_ => "public, max-age=600",
@@ -484,6 +486,28 @@ mod cache_fast_path_tests {
response
.headers_mut()
.insert("content-type", HeaderValue::from_static("text/plain"));
if matches!(
parts.uri.path(),
"/cache-headers" | "/cache-headers-revalidate"
) {
for (name, value) in [
("x-cache", "backend-MISS"),
("x-cache-hits", "14"),
("x-cache-status", "origin-status"),
("x-cache-reason", "origin_reason"),
("x-cache-resident-age", "16"),
] {
response.headers_mut().insert(name, value.parse().unwrap());
}
response.headers_mut().append(
"cache-status",
HeaderValue::from_static("InnerCache; hit; ttl=120"),
);
response.headers_mut().append(
"cache-status",
HeaderValue::from_static("OriginCache; fwd=miss"),
);
}
for name in [
"range",
"if-range",
@@ -577,6 +601,124 @@ mod cache_fast_path_tests {
fixture.stop().await;
}
fn cache_chain(headers: &HeaderMap) -> Vec<&str> {
headers
.get_all("cache-status")
.iter()
.map(|value| value.to_str().unwrap())
.collect()
}
#[tokio::test]
async fn origin_cache_headers_survive_bypass_and_hot_storage_when_rustproxy_debug_is_off() {
for cache_enabled in [false, true] {
let origin = origin().await;
let mut config = config_for(origin.addr, cache_enabled);
config.cache.expose_status = false;
config.cache.expose_hits = false;
config.cache.expose_cache_status = false;
config.cache.expose_resident_age = false;
let fixture = Fixture::new(config).await;
for _ in 0..2 {
let response = fixture.get("/cache-headers", &[]).await;
assert_eq!(response.status, StatusCode::OK);
assert_eq!(response.headers["x-cache"], "backend-MISS");
assert_eq!(response.headers["x-cache-hits"], "14");
assert_eq!(response.headers["x-cache-status"], "origin-status");
assert_eq!(response.headers["x-cache-reason"], "origin_reason");
assert_eq!(response.headers["x-cache-resident-age"], "16");
assert_eq!(
cache_chain(&response.headers),
vec!["InnerCache; hit; ttl=120", "OriginCache; fwd=miss",]
);
}
assert_eq!(
origin.requests.load(Ordering::Relaxed),
if cache_enabled { 1 } else { 2 }
);
fixture.stop().await;
}
}
#[tokio::test]
async fn origin_and_rustproxy_diagnostics_coexist_on_miss_hit_and_hit_only() {
for hit_only in [false, true] {
let origin = origin().await;
let mut config = config_for(origin.addr, true);
config.cache.expose_only_hits = hit_only;
config.cache.expose_resident_age = true;
let fixture = Fixture::new(config).await;
let miss = fixture.get("/cache-headers", &[]).await;
assert_eq!(miss.status, StatusCode::OK);
assert_eq!(
miss.headers["x-cache"],
if hit_only { "backend-MISS" } else { "MISS" }
);
assert_eq!(miss.headers["x-cache-status"], "origin-status");
let miss_chain = cache_chain(&miss.headers);
assert_eq!(
&miss_chain[..2],
&["InnerCache; hit; ttl=120", "OriginCache; fwd=miss"][..]
);
if hit_only {
assert_eq!(miss_chain.len(), 2);
assert!(!miss.headers.contains_key("x-upstream-cache"));
} else {
assert_eq!(miss.headers["x-upstream-cache"], "backend-MISS");
assert_eq!(miss.headers["x-upstream-cache-hits"], "14");
assert!(miss_chain[2].starts_with("RustProxy; fwd=miss; detail="));
}
let hit = fixture.get("/cache-headers", &[]).await;
assert_eq!(hit.status, StatusCode::OK);
assert_eq!(hit.headers["x-cache"], "HIT");
assert_eq!(hit.headers["x-upstream-cache"], "backend-MISS");
assert_eq!(hit.headers["x-upstream-cache-hits"], "14");
assert_eq!(hit.headers["x-upstream-cache-reason"], "origin_reason");
assert_eq!(hit.headers["x-upstream-cache-resident-age"], "16");
assert_eq!(hit.headers["x-cache-status"], "origin-status");
let hit_chain = cache_chain(&hit.headers);
assert_eq!(
&hit_chain[..2],
&["InnerCache; hit; ttl=120", "OriginCache; fwd=miss"][..]
);
assert_eq!(hit_chain.len(), 3);
assert!(hit_chain[2].starts_with("RustProxy; hit; ttl="));
assert!(hit_chain[2].ends_with("; detail=\"cache_hit\""));
assert_eq!(origin.requests.load(Ordering::Relaxed), 1);
fixture.stop().await;
}
}
#[tokio::test]
async fn origin_cache_status_survives_conditional_304_and_hot_refresh() {
let origin = origin().await;
let mut config = config_for(origin.addr, true);
config.cache.default_ttl_seconds = 0;
let fixture = Fixture::new(config).await;
let first = fixture.get("/cache-headers-revalidate", &[]).await;
assert_eq!(first.headers["x-cache"], "MISS");
assert_eq!(first.headers["x-upstream-cache"], "backend-MISS");
assert_eq!(cache_chain(&first.headers).len(), 3);
let validated = fixture.get("/cache-headers-revalidate", &[]).await;
assert_eq!(validated.status, StatusCode::OK);
assert_eq!(validated.headers["x-cache"], "HIT");
assert_eq!(validated.headers["x-cache-reason"], "cache_revalidated");
assert_eq!(validated.headers["x-upstream-cache"], "backend-MISS");
assert_eq!(validated.headers["x-cache-status"], "origin-status");
let chain = cache_chain(&validated.headers);
assert_eq!(
&chain[..2],
&["InnerCache; hit; ttl=120", "OriginCache; fwd=miss"][..]
);
assert_eq!(chain.len(), 3);
assert_eq!(
chain[2],
"RustProxy; fwd=stale; fwd-status=304; detail=\"cache_revalidated\""
);
assert_eq!(origin.requests.load(Ordering::Relaxed), 2);
fixture.stop().await;
}
#[tokio::test]
async fn proxy_cache_on_preserves_vary_private_revalidation_and_ranges() {
let origin = origin().await;
@@ -648,6 +790,7 @@ mod cache_fast_path_tests {
.get("/missing", &[("cache-control", "no-cache, only-if-cached")])
.await;
assert_eq!(miss.status, StatusCode::GATEWAY_TIMEOUT);
assert!(!miss.headers.contains_key("cache-status"));
assert_eq!(
origin.requests.load(Ordering::Relaxed),
0,
+8 -9
View File
@@ -26,8 +26,7 @@ if let Some(ticket) = &cache_ticket {
} else {
"cache_hit"
};
// only-if-cached forbids any origin communication, including
// asynchronous grace validation and proactive refresh.
if hit.background_revalidate && !only_cached {
if let (Some(cached), Some(guard)) = (
cached_revalidation.clone(),
@@ -108,14 +107,14 @@ if let Some(ticket) = &cache_ticket {
.lock()
.unwrap_or_else(|e| e.into_inner()) = reason;
}
cache_settings.annotate_reason(
cache_settings.annotate_reason_transformed(
hit.response.headers_mut(),
"HIT",
hit.hits,
hit.remaining_ttl,
reason,
hit.transformed,
);
cache_settings.annotate_transformed(hit.response.headers_mut(), hit.transformed);
if hit.transformed {
state.transform_metrics.record_cache_hit(scope);
}
@@ -165,14 +164,14 @@ let serve_stale = |reason: &'static str, elapsed: Duration, origin_backend: &str
if let crate::cache::RangeSelection::Single(range) = range_selection {
hit = hit.apply_range(range, if_range.as_ref());
}
cache_settings.annotate_reason(
cache_settings.annotate_reason_transformed(
hit.response.headers_mut(),
"HIT",
hit.hits,
0,
reason,
hit.transformed,
);
cache_settings.annotate_transformed(hit.response.headers_mut(), hit.transformed);
if hit.transformed {
state.transform_metrics.record_cache_hit(scope);
}
@@ -198,6 +197,7 @@ if only_cached {
"RustProxy: no reusable cached response\n",
);
cache_settings.annotate(response.headers_mut(), "MISS", 0, 0);
response.headers_mut().remove("cache-status");
return response;
}
@@ -254,15 +254,14 @@ if handshake.is_none()
if conditional {
hit.response = crate::cache::make_not_modified(hit.response);
}
cache_settings.annotate_reason(
cache_settings.annotate_reason_transformed(
hit.response.headers_mut(),
"HIT",
hit.hits,
hit.remaining_ttl,
reason,
hit.transformed,
);
cache_settings
.annotate_transformed(hit.response.headers_mut(), hit.transformed);
if hit.transformed {
state.transform_metrics.record_cache_hit(scope);
}
+7 -8
View File
@@ -21,15 +21,14 @@ if response.status() == StatusCode::NOT_MODIFIED && revalidation_sent {
}
let resident_age = hit.resident_age_seconds;
let mut response = hit.response;
cache_settings.annotate_reason(
cache_settings.annotate_reason_transformed(
response.headers_mut(),
"HIT",
hit.hits,
hit.remaining_ttl,
"cache_revalidated",
hit.transformed,
);
cache_settings
.annotate_transformed(response.headers_mut(), hit.transformed);
if hit.transformed {
state.transform_metrics.record_cache_hit(scope);
}
@@ -46,15 +45,14 @@ if response.status() == StatusCode::NOT_MODIFIED && revalidation_sent {
} else {
cached.response(merged)
};
cache_settings.annotate_reason(
cache_settings.annotate_reason_transformed(
response.headers_mut(),
"HIT",
0,
0,
"cache_revalidated_uncached",
cached.transformed(),
);
cache_settings
.annotate_transformed(response.headers_mut(), cached.transformed());
if cached.transformed() {
state.transform_metrics.record_cache_hit(scope);
}
@@ -184,7 +182,8 @@ let transformed = response
.extensions()
.get::<crate::transform::CachedTransformed>()
.is_some();
cache_settings.annotate_reason(response.headers_mut(), outcome, 0, 0, reason);
cache_settings.annotate_transformed(response.headers_mut(), transformed);
cache_settings.annotate_reason_transformed(
response.headers_mut(), outcome, 0, 0, reason, transformed,
);
response
}
+5 -5
View File
@@ -96,9 +96,8 @@
if request_method == Method::HEAD {
*response.body_mut() = Body::empty();
}
cache_settings.annotate_reason(response.headers_mut(), "HIT", hit.hits,
hit.remaining_ttl, "cache_hit");
cache_settings.annotate_transformed(response.headers_mut(), hit.transformed);
cache_settings.annotate_reason_transformed(response.headers_mut(), "HIT", hit.hits,
hit.remaining_ttl, "cache_hit", hit.transformed);
cache_settings.annotate_resident_age(response.headers_mut(), hit.resident_age_seconds);
state.cache.record(scope, &host, "HIT", "cache_hit");
if hit.transformed { state.transform_metrics.record_cache_hit(scope); }
@@ -257,9 +256,10 @@
*exchange.cache_reason.lock().unwrap_or_else(|e| e.into_inner()) = reason;
}
state.cache.record(scope, &host, outcome, reason);
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);
cache_settings.annotate_reason_transformed(
response.headers_mut(), outcome, 0, 0, reason, transformed,
);
#[cfg(target_os = "linux")]
if !enabled && identity_request && request_method == Method::GET
&& !transform_settings.is_some_and(|settings| settings.enabled)
+10 -1
View File
@@ -322,13 +322,22 @@ mod authority_tests {
("etag", "\"old\""),
("x-cache", "HIT"),
("x-cache-reason", "origin"),
("x-upstream-cache", "origin-MISS"),
("x-upstream-cache-hits", "3"),
("cache-status", "Origin; hit"),
] {
response.headers_mut().insert(name, value.parse().unwrap());
}
enforce_local_page_no_cache(&mut response);
assert_eq!(response.headers().get("cache-control").unwrap(), "no-store");
for name in ["etag", "x-cache", "x-cache-reason", "cache-status"] {
for name in [
"etag",
"x-cache",
"x-cache-reason",
"x-upstream-cache",
"x-upstream-cache-hits",
"cache-status",
] {
assert!(!response.headers().contains_key(name), "{name}");
}
assert!(response
+18 -2
View File
@@ -258,6 +258,12 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
"x-cache-hits",
"x-cache-status",
"x-cache-resident-age",
"x-cache-reason",
"x-upstream-cache",
"x-upstream-cache-hits",
"x-upstream-cache-reason",
"x-upstream-cache-resident-age",
"cache-status",
"content-encoding",
"vary",
"strict-transport-security",
@@ -395,7 +401,6 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
.cache_reason
.lock()
.unwrap_or_else(|e| e.into_inner());
cache.annotate_reason(response.headers_mut(), outcome, 0, 0, reason);
let transformed = response
.extensions()
.get::<crate::transform::Applied>()
@@ -404,7 +409,14 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
.extensions()
.get::<crate::transform::CachedTransformed>()
.is_some();
cache.annotate_transformed(response.headers_mut(), transformed);
cache.annotate_reason_transformed(
response.headers_mut(),
outcome,
0,
0,
reason,
transformed,
);
}
}
if !minimal_welcome {
@@ -441,6 +453,10 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
"x-cache-status",
"x-cache-resident-age",
"x-cache-reason",
"x-upstream-cache",
"x-upstream-cache-hits",
"x-upstream-cache-reason",
"x-upstream-cache-resident-age",
"cache-status",
] {
response.headers_mut().remove(name);
+8 -2
View File
@@ -129,8 +129,14 @@ pub(super) fn finish_web_revalidation(
if *method == Method::HEAD {
*response.body_mut() = Body::empty();
}
settings.annotate_reason(response.headers_mut(), "HIT", hits, ttl, reason);
settings.annotate_transformed(response.headers_mut(), transformed);
settings.annotate_reason_transformed(
response.headers_mut(),
"HIT",
hits,
ttl,
reason,
transformed,
);
settings.annotate_resident_age(response.headers_mut(), resident_age);
Some(WebRevalidated {
response,