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

+84
View File
@@ -62,6 +62,90 @@ mod cold_tier_tests {
panic!("cold tier did not persist in time");
}
#[tokio::test]
async fn upstream_cache_headers_survive_cold_demotion_and_hit_annotation() {
let dir = tempfile::tempdir().unwrap();
let settings = cold_settings();
let shutdown = CancellationToken::new();
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
shutdown.clone(),
);
let ticket = cache
.ticket_scoped(
11,
"cold-test",
"http",
"cold.example",
"origin@127.0.0.1:8080",
"/origin-cache",
&HeaderMap::new(),
)
.unwrap();
let mut response = Response::new(Body::from("cached-origin-body"));
response.headers_mut().insert(
"cache-control",
HeaderValue::from_static("public, max-age=120"),
);
response.headers_mut().append(
"cache-status",
HeaderValue::from_static("OriginCache; hit; ttl=300"),
);
response
.headers_mut()
.insert("x-cache", HeaderValue::from_static("Origin-HIT"));
let plan = plan(
StatusCode::OK,
response.headers(),
&settings,
Duration::ZERO,
)
.unwrap();
cache
.wrap(response, ticket.clone(), plan, &settings)
.into_body()
.collect()
.await
.unwrap();
fill(&cache, &settings, "/second", "other-cached-body").await;
wait_for_cold(&cache, 1).await;
let mut hit = cache
.get_tiered(&ticket, &settings)
.await
.expect("COLD hit");
assert_eq!(hit.response.headers()["x-cache"], "Origin-HIT");
assert_eq!(
hit.response.headers()["cache-status"],
"OriginCache; hit; ttl=300"
);
let display = Settings {
expose_status: true,
expose_cache_status: true,
..settings.clone()
};
display.annotate_reason_transformed(
hit.response.headers_mut(),
"HIT",
hit.hits,
hit.remaining_ttl,
"cache_hit",
false,
);
assert_eq!(hit.response.headers()["x-cache"], "HIT");
assert_eq!(hit.response.headers()["x-upstream-cache"], "Origin-HIT");
assert_eq!(
hit.response
.headers()
.get_all("cache-status")
.iter()
.count(),
2
);
shutdown.cancel();
}
#[tokio::test]
async fn selective_purge_removes_matching_cold_object() {
let dir = tempfile::tempdir().unwrap();
+200 -31
View File
@@ -103,7 +103,10 @@ mod dashboard_tests {
}
#[test]
fn bypass_reasons_are_fixed_and_private_inputs_are_not_echoed() {
let settings = Settings { varnish_compatible: false, ..Settings::default() };
let settings = Settings {
varnish_compatible: false,
..Settings::default()
};
let mut headers = HeaderMap::new();
headers.insert("cookie", HeaderValue::from_static("session=very-private"));
assert_eq!(
@@ -125,13 +128,15 @@ mod dashboard_tests {
expose_cache_status: true,
..settings
};
diagnostics.annotate_reason(&mut headers, "BYPASS", 0, 0, "request_cookie");
assert_eq!(headers["x-cache-reason"], "request_cookie");
assert_eq!(
headers["cache-status"],
"RustProxy; fwd=bypass; detail=\"request_cookie\""
diagnostics.annotate_reason_transformed(
&mut headers,
"BYPASS",
0,
0,
"request_cookie",
true,
);
diagnostics.annotate_transformed(&mut headers, true);
assert_eq!(headers["x-cache-reason"], "request_cookie");
assert_eq!(
headers["cache-status"],
"RustProxy; fwd=bypass; detail=\"request_cookie\"; transformed"
@@ -149,29 +154,39 @@ mod dashboard_tests {
};
let mut headers = HeaderMap::new();
headers.insert("x-cache", HeaderValue::from_static("untrusted-upstream"));
headers.insert(
"cache-status",
HeaderValue::from_static("untrusted-upstream"),
headers.insert("cache-status", HeaderValue::from_static("Origin; hit"));
global.annotate_reason_transformed(
&mut headers,
"MISS",
0,
0,
"response_headers_policy",
true,
);
global.annotate_reason(&mut headers, "MISS", 0, 0, "response_headers_policy");
global.annotate_transformed(&mut headers, true);
for name in [
"x-cache",
"x-cache-hits",
"cache-status",
"x-cache-reason",
"x-cache-resident-age",
] {
assert!(!headers.contains_key(name), "{name} leaked on MISS");
}
assert_eq!(headers["x-cache"], "untrusted-upstream");
assert_eq!(headers["cache-status"], "Origin; hit");
assert!(!headers.contains_key("x-cache-hits"));
assert!(!headers.contains_key("x-cache-reason"));
assert_eq!(headers["via"], "RustProxy");
global.annotate_reason(&mut headers, "BYPASS", 0, 0, "request_cookie");
assert!(!headers.contains_key("cache-status"));
assert_eq!(headers.get_all("cache-status").iter().count(), 1);
global.annotate_reason(&mut headers, "HIT", 4, 60, "cache_hit");
global.annotate_resident_age(&mut headers, 10);
assert_eq!(headers["x-cache"], "HIT");
assert_eq!(headers["x-cache-hits"], "4");
assert!(headers["cache-status"].to_str().unwrap().contains("; hit;"));
let chain: Vec<_> = headers
.get_all("cache-status")
.iter()
.map(|value| value.to_str().unwrap())
.collect();
assert_eq!(
chain,
vec![
"Origin; hit",
"RustProxy; hit; ttl=60; detail=\"cache_hit\""
]
);
assert_eq!(headers["x-upstream-cache"], "untrusted-upstream");
assert_eq!(headers["x-cache-resident-age"], "10");
let mut override_on = Overrides {
expose_only_hits: Some(false),
@@ -210,7 +225,7 @@ mod dashboard_tests {
}
#[test]
fn disabled_cache_strips_diagnostics_without_exposing_bypass_headers() {
fn disabled_cache_forwards_origin_diagnostics_unchanged() {
let settings = Settings {
enabled: false,
expose_status: true,
@@ -228,16 +243,170 @@ mod dashboard_tests {
headers.insert("x-cache-resident-age", HeaderValue::from_static("5"));
settings.annotate_reason(&mut headers, "BYPASS", 0, 0, "cache_disabled");
settings.annotate_resident_age(&mut headers, 9);
for name in [
"x-cache",
"x-cache-hits",
"x-cache-status",
assert_eq!(headers["x-cache"], "origin");
assert_eq!(headers["x-cache-hits"], "99");
assert_eq!(headers["x-cache-status"], "origin");
assert_eq!(headers["cache-status"], "Origin; hit");
assert_eq!(headers["x-cache-reason"], "origin");
assert_eq!(headers["x-cache-resident-age"], "5");
assert!(!headers.contains_key("x-upstream-cache"));
assert!(!headers.contains_key("via"));
}
#[test]
fn chain_and_legacy_header_collisions_keep_origin_values() {
let settings = Settings {
expose_status: true,
expose_hits: true,
expose_cache_status: true,
expose_resident_age: true,
..Settings::default()
};
let mut headers = HeaderMap::new();
headers.append(
"cache-status",
"x-cache-reason",
"x-cache-resident-age",
HeaderValue::from_static("Inner; hit; ttl=100"),
);
headers.append("cache-status", HeaderValue::from_static("Shield; fwd=miss"));
for (name, value) in [
("x-cache", "BACKEND-MISS"),
("x-cache-hits", "21"),
("x-cache-reason", "origin_reason"),
("x-cache-resident-age", "17"),
("x-cache-status", "backend-status"),
] {
assert!(!headers.contains_key(name), "{name}");
headers.insert(name, value.parse().unwrap());
}
settings.annotate_reason_transformed(&mut headers, "HIT", 3, 482, "cache_hit", true);
settings.annotate_resident_age(&mut headers, 9);
let cache_chain: Vec<_> = headers
.get_all("cache-status")
.iter()
.map(|value| value.to_str().unwrap())
.collect();
assert_eq!(
cache_chain,
vec![
"Inner; hit; ttl=100",
"Shield; fwd=miss",
"RustProxy; hit; ttl=482; detail=\"cache_hit\"; transformed",
]
);
assert_eq!(headers["x-cache"], "HIT");
assert_eq!(headers["x-upstream-cache"], "BACKEND-MISS");
assert_eq!(headers["x-cache-hits"], "3");
assert_eq!(headers["x-upstream-cache-hits"], "21");
assert_eq!(headers["x-cache-reason"], "cache_hit");
assert_eq!(headers["x-upstream-cache-reason"], "origin_reason");
assert_eq!(headers["x-cache-resident-age"], "9");
assert_eq!(headers["x-upstream-cache-resident-age"], "17");
assert_eq!(headers["x-cache-status"], "backend-status");
}
#[test]
fn diagnostic_options_do_not_hide_backend_cache_status() {
for (enabled, status, only_hits) in [
(true, false, false),
(true, true, true),
(false, true, false),
] {
let settings = Settings {
enabled,
expose_status: status,
expose_cache_status: status,
expose_only_hits: only_hits,
..Settings::default()
};
let mut headers = HeaderMap::new();
headers.insert("x-cache", HeaderValue::from_static("Backend; MISS"));
headers.insert("cache-status", HeaderValue::from_static("Origin; fwd=miss"));
settings.annotate_reason_transformed(&mut headers, "MISS", 0, 0, "cache_miss", true);
assert_eq!(headers["x-cache"], "Backend; MISS");
assert_eq!(headers["cache-status"], "Origin; fwd=miss");
assert_eq!(headers.get_all("cache-status").iter().count(), 1);
assert!(!headers.contains_key("x-upstream-cache"));
}
}
#[test]
fn repeated_upstream_legacy_values_are_not_collapsed_or_discarded() {
let settings = Settings {
expose_status: true,
expose_cache_status: true,
..Settings::default()
};
let mut headers = HeaderMap::new();
headers.append("x-cache", HeaderValue::from_static("inner-HIT"));
headers.append("x-cache", HeaderValue::from_static("outer-MISS"));
headers.append("cache-status", HeaderValue::from_static("Inner; hit"));
headers.append("cache-status", HeaderValue::from_static("Outer; fwd=miss"));
settings.annotate_reason(&mut headers, "MISS", 0, 0, "cache_miss");
let upstream_values: Vec<_> = headers
.get_all("x-upstream-cache")
.iter()
.map(|value| value.to_str().unwrap())
.collect();
assert_eq!(upstream_values, vec!["inner-HIT", "outer-MISS"]);
assert_eq!(headers["x-cache"], "MISS");
let status_chain: Vec<_> = headers
.get_all("cache-status")
.iter()
.map(|value| value.to_str().unwrap())
.collect();
assert_eq!(
status_chain,
vec![
"Inner; hit",
"Outer; fwd=miss",
"RustProxy; fwd=miss; detail=\"cache_miss\"",
]
);
}
#[test]
fn client_no_cache_miss_does_not_claim_a_fresh_candidate() {
let settings = Settings {
expose_cache_status: true,
..Settings::default()
};
let mut headers = HeaderMap::new();
settings.annotate_reason(&mut headers, "MISS", 0, 0, "request_no_cache");
assert_eq!(
headers["cache-status"],
"RustProxy; fwd=miss; detail=\"request_no_cache\"",
);
}
#[test]
fn revalidated_304_is_forwarded_under_rfc_9211_even_if_legacy_x_cache_is_hit() {
let settings = Settings {
expose_status: true,
expose_cache_status: true,
..Settings::default()
};
let mut headers = HeaderMap::new();
headers.insert("cache-status", HeaderValue::from_static("Origin; hit"));
settings.annotate_reason_transformed(
&mut headers,
"HIT",
2,
50,
"cache_revalidated",
false,
);
assert_eq!(headers["x-cache"], "HIT");
let chain: Vec<_> = headers
.get_all("cache-status")
.iter()
.map(|v| v.to_str().unwrap())
.collect();
assert_eq!(
chain,
vec![
"Origin; hit",
"RustProxy; fwd=stale; fwd-status=304; detail=\"cache_revalidated\"",
]
);
}
#[test]
+1 -8
View File
@@ -97,14 +97,7 @@ impl Candidate {
self.headers.insert("date", value);
}
}
for name in [
"x-request-id",
"x-cache",
"x-cache-hits",
"x-cache-status",
"x-cache-reason",
"cache-status",
] {
for name in ["x-request-id"] {
self.headers.remove(name);
}
let vary = std::mem::take(&mut self.plan.vary);
+1 -8
View File
@@ -27,14 +27,7 @@ impl Cache {
headers.insert("date", value);
}
}
for name in [
"x-request-id",
"x-cache",
"x-cache-hits",
"x-cache-status",
"x-cache-reason",
"cache-status",
] {
for name in ["x-request-id"] {
headers.remove(name);
}
let hits = cached.hits.saturating_add(1);
+87 -74
View File
@@ -171,16 +171,41 @@ impl Settings {
.any(|item| item.eq_ignore_ascii_case(extension))
}
pub fn annotate(&self, headers: &mut HeaderMap, outcome: &'static str, hits: u64, ttl: u64) {
for name in [
"x-cache",
"x-cache-hits",
"x-cache-status",
"cache-status",
"x-cache-reason",
"x-cache-resident-age",
] {
headers.remove(name);
}
self.annotate_internal(headers, outcome, hits, ttl, None, false);
}
pub fn annotate_reason(
&self,
headers: &mut HeaderMap,
outcome: &'static str,
hits: u64,
ttl: u64,
reason: &'static str,
) {
self.annotate_internal(headers, outcome, hits, ttl, Some(reason), false);
}
pub fn annotate_reason_transformed(
&self,
headers: &mut HeaderMap,
outcome: &'static str,
hits: u64,
ttl: u64,
reason: &'static str,
transformed: bool,
) {
self.annotate_internal(headers, outcome, hits, ttl, Some(reason), transformed);
}
fn annotate_internal(
&self,
headers: &mut HeaderMap,
outcome: &'static str,
hits: u64,
ttl: u64,
reason: Option<&'static str>,
transformed: bool,
) {
if !self.enabled {
return;
}
@@ -196,89 +221,77 @@ impl Settings {
return;
}
if self.expose_status {
headers.insert("x-cache", HeaderValue::from_static(outcome));
insert_cache_diagnostic(
headers,
"x-cache",
"x-upstream-cache",
HeaderValue::from_static(outcome),
);
if let Some(reason) = reason {
insert_cache_diagnostic(
headers,
"x-cache-reason",
"x-upstream-cache-reason",
HeaderValue::from_static(reason),
);
}
}
if self.expose_hits {
headers.insert(
insert_cache_diagnostic(
headers,
"x-cache-hits",
"x-upstream-cache-hits",
HeaderValue::from_str(&hits.to_string()).expect("integer header"),
);
}
if self.expose_cache_status {
let value = match outcome {
"HIT" => format!("RustProxy; hit; ttl={ttl}"),
"MISS" => "RustProxy; fwd=uri-miss".into(),
_ => "RustProxy; fwd=bypass".into(),
let mut value = if matches!(
reason,
Some("cache_revalidated" | "cache_revalidated_uncached")
) {
"RustProxy; fwd=stale; fwd-status=304".to_owned()
} else {
match outcome {
"MISS" => "RustProxy; fwd=miss".to_owned(),
_ => "RustProxy; fwd=bypass".to_owned(),
}
};
headers.insert(
if let Some(reason) = reason {
value.push_str(&format!("; detail=\"{reason}\""));
}
if transformed {
value.push_str("; transformed");
}
headers.append(
"cache-status",
HeaderValue::from_str(&value).expect("generated cache status"),
HeaderValue::from_str(&value).expect("generated Cache-Status member"),
);
}
}
pub fn annotate_resident_age(&self, headers: &mut HeaderMap, resident_age: u64) {
headers.remove("x-cache-resident-age");
if !self.enabled {
return;
}
if self.expose_resident_age {
headers.insert(
if self.enabled && self.expose_resident_age {
insert_cache_diagnostic(
headers,
"x-cache-resident-age",
"x-upstream-cache-resident-age",
HeaderValue::from_str(&resident_age.to_string()).expect("integer header"),
);
}
}
pub fn annotate_transformed(&self, headers: &mut HeaderMap, transformed: bool) {
if !self.enabled
|| !self.expose_cache_status
|| !transformed
|| (self.expose_only_hits
&& !headers
.get("cache-status")
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.starts_with("RustProxy; hit")))
{
return;
}
let base = headers
.get("cache-status")
.and_then(|value| value.to_str().ok())
.unwrap_or("RustProxy");
if base
.split(';')
.any(|part| part.trim().eq_ignore_ascii_case("transformed"))
{
return;
}
if let Ok(value) = format!("{base}; transformed").parse() {
headers.insert("cache-status", value);
}
}
pub fn annotate_reason(
&self,
headers: &mut HeaderMap,
outcome: &'static str,
hits: u64,
ttl: u64,
reason: &'static str,
) {
self.annotate(headers, outcome, hits, ttl);
if !self.enabled || (self.expose_only_hits && outcome != "HIT") {
return;
}
if self.expose_status {
headers.insert("x-cache-reason", HeaderValue::from_static(reason));
}
if self.expose_cache_status {
let base = headers
.get("cache-status")
.and_then(|v| v.to_str().ok())
.unwrap_or("RustProxy");
if let Ok(value) = format!("{base}; detail=\"{reason}\"").parse() {
headers.insert("cache-status", value);
}
}
}
fn insert_cache_diagnostic(
headers: &mut HeaderMap,
name: &'static str,
upstream_name: &'static str,
value: HeaderValue,
) {
let upstream_values: Vec<HeaderValue> = headers.get_all(name).iter().cloned().collect();
for upstream_value in upstream_values {
headers.append(upstream_name, upstream_value);
}
headers.insert(name, value);
}
fn valid_relative_cache_path(value: &str) -> bool {
if value.is_empty() || value.len() > 256 {