This commit is contained in:
Mateusz Gruszczyński committed 2026-10-22 15:31:47 +02:00
1 parent 3ea00045bf
commit 1cd6b7ac99
22 files changed
+550 -149

No files matched your search

+88 -6
View File
@@ -470,10 +470,12 @@ mod cache_fast_path_tests {
.insert("etag", HeaderValue::from_static("\"origin-v1\""));
response.headers_mut().insert(
"cache-control",
HeaderValue::from_static(if parts.uri.path() == "/private" {
"private, max-age=600"
} else {
"public, max-age=600"
HeaderValue::from_static(match parts.uri.path() {
"/private" => "private, max-age=600",
"/backend-no-cache" => "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",
}),
);
response
@@ -612,7 +614,7 @@ mod cache_fast_path_tests {
.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-cache-reason"], "cache_hit", "default compatibility mode must not revalidate a fresh object");
assert_eq!(validated.body, "origin-a");
let conditional = fixture
.get(
@@ -631,7 +633,87 @@ mod cache_fast_path_tests {
"MISS"
);
}
assert_eq!(origin.requests.load(Ordering::Relaxed), 6);
assert_eq!(origin.requests.load(Ordering::Relaxed), 5);
fixture.stop().await;
}
#[tokio::test]
async fn compatible_only_if_cached_on_miss_never_contacts_origin() {
let origin = origin().await;
let fixture = Fixture::new(config_for(origin.addr, true)).await;
let miss = fixture.get("/missing", &[("cache-control", "no-cache, only-if-cached")]).await;
assert_eq!(miss.status, StatusCode::GATEWAY_TIMEOUT);
assert_eq!(origin.requests.load(Ordering::Relaxed), 0, "only-if-cached MISS must not contact backend");
let primed = fixture.get("/vary", &[]).await;
assert_eq!(primed.headers["x-cache"], "MISS");
let hit = fixture.get("/vary", &[("cache-control", "only-if-cached")]).await;
assert_eq!(hit.headers["x-cache"], "HIT");
assert_eq!(origin.requests.load(Ordering::Relaxed), 1);
let blocked = fixture.get("/vary", &[("cache-control", "no-store")]).await;
assert_eq!(blocked.headers["x-cache-reason"], "request_no_store");
assert_eq!(origin.requests.load(Ordering::Relaxed), 2, "no-store must bypass even a fresh HIT");
fixture.stop().await;
}
#[tokio::test]
async fn compatible_origin_no_cache_preserves_etag_revalidation_and_fallback_ttl() {
let origin = origin().await;
let mut config = config_for(origin.addr, true);
config.cache.default_ttl_seconds = 0;
let fixture = Fixture::new(config).await;
assert_eq!(fixture.get("/backend-no-cache", &[]).await.headers["x-cache"], "MISS");
let cached_only = fixture
.get("/backend-no-cache", &[("cache-control", "only-if-cached")])
.await;
assert_eq!(cached_only.status, StatusCode::GATEWAY_TIMEOUT);
assert_eq!(origin.requests.load(Ordering::Relaxed), 1);
let validated = fixture.get("/backend-no-cache", &[]).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!(origin.requests.load(Ordering::Relaxed), 2);
fixture.stop().await;
let origin = origin().await;
let mut config = config_for(origin.addr, true);
config.cache.default_ttl_seconds = 45;
let fixture = Fixture::new(config).await;
assert_eq!(fixture.get("/backend-no-cache", &[]).await.headers["x-cache"], "MISS");
let reused = fixture.get("/backend-no-cache", &[]).await;
assert_eq!(reused.headers["x-cache"], "HIT");
assert_eq!(reused.headers["x-cache-reason"], "cache_hit");
assert_eq!(origin.requests.load(Ordering::Relaxed), 1, "configured fallback TTL avoids origin revalidation");
fixture.stop().await;
}
#[tokio::test]
async fn only_if_cached_stale_grace_never_background_refreshes_origin() {
let origin = origin().await;
let fixture = Fixture::new(config_for(origin.addr, true)).await;
assert_eq!(fixture.get("/grace", &[]).await.headers["x-cache"], "MISS");
tokio::time::sleep(Duration::from_millis(1200)).await;
let stale = fixture.get("/grace", &[("cache-control", "only-if-cached")]).await;
assert_eq!(stale.status, StatusCode::OK);
assert_eq!(stale.headers["x-cache"], "HIT");
assert_eq!(stale.headers["x-cache-reason"], "cache_stale_while_revalidate");
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(origin.requests.load(Ordering::Relaxed), 1, "only-if-cached must not start async validation");
fixture.stop().await;
}
#[tokio::test]
async fn web_server_only_if_cached_on_miss_never_reads_origin() {
let directory = TempDir::new().unwrap();
let fixture = Fixture::new(web_config(&directory, true)).await;
let miss = fixture.get("/style.css", &[("cache-control", "only-if-cached")]).await;
assert_eq!(miss.status, StatusCode::GATEWAY_TIMEOUT);
assert_eq!(fixture.get("/style.css", &[]).await.headers["x-cache"], "MISS");
let hit = fixture.get("/style.css", &[("cache-control", "only-if-cached")]).await;
assert_eq!(hit.headers["x-cache"], "HIT");
let bypass = fixture.get("/style.css", &[("cache-control", "no-store, only-if-cached")]).await;
assert_eq!(bypass.status, StatusCode::GATEWAY_TIMEOUT, "uncacheable only-if-cached must not read the Web Server file");
let reload = fixture.get("/style.css", &[("cache-control", "no-cache")]).await;
assert_eq!(reload.headers["x-cache-reason"], "cache_hit");
fixture.stop().await;
}
+4 -1
View File
@@ -26,7 +26,9 @@ if let Some(ticket) = &cache_ticket {
} else {
"cache_hit"
};
if hit.background_revalidate {
// 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(),
cached_revalidation
@@ -66,6 +68,7 @@ if let Some(ticket) = &cache_ticket {
}
}
if cache_settings.proactive_refresh_enabled
&& !only_cached
&& !hit.stale
&& hit.remaining_ttl > 0
{
+2 -3
View File
@@ -177,9 +177,8 @@ let original_policy = cache_plan.request_policy(
request.uri().query().is_some(), cache_settings,
);
let only_cached =
cache_settings.active_for(route.cache) && !cache_settings.varnish_compatible
&& crate::cache::only_if_cached(request.headers());
let only_cached = cache_settings.active_for(route.cache)
&& crate::cache::only_if_cached(request.headers());
let origin_suffix = match route.rewritten_path(&path) {
Ok(None) => public_suffix.clone(),
Ok(Some(rewritten)) => {
+1 -2
View File
@@ -113,8 +113,7 @@
}
}
}
if cache_ticket.is_some() && !cache_settings.varnish_compatible
&& crate::cache::only_if_cached(&request_headers) {
if enabled && crate::cache::only_if_cached(&request_headers) {
return error(StatusCode::GATEWAY_TIMEOUT,
"RustProxy: only-if-cached request has no usable Web Server representation\n");
}