0.34.13_cleaup_code
This commit is contained in:
1 parent
ec46a96a29
commit
5c2e50ec06
87 files changed
+4890
-1338
No files matched your search
Vendored
+60
-53
@@ -70,9 +70,13 @@ impl Cache {
|
||||
continue;
|
||||
};
|
||||
let body_path = shard.join(&file.body_file);
|
||||
let expected_scope_revision = self.scope_revisions
|
||||
.lock().unwrap_or_else(|e| e.into_inner())
|
||||
.get(&file.scope).copied().unwrap_or(0);
|
||||
let expected_scope_revision = self
|
||||
.scope_revisions
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.get(&file.scope)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
if file.format != COLD_FORMAT_VERSION
|
||||
|| file.revision != revision
|
||||
|| file.scope_revision != expected_scope_revision
|
||||
@@ -142,57 +146,60 @@ impl Cache {
|
||||
let _ = std::fs::remove_file(&body_path);
|
||||
continue;
|
||||
}
|
||||
loaded.push((key, ColdEntry {
|
||||
base_key,
|
||||
vary,
|
||||
revision,
|
||||
scope: file.scope,
|
||||
scheme: file.scheme,
|
||||
authority: if file.authority.is_empty() {
|
||||
file.domain.clone()
|
||||
} else {
|
||||
file.authority
|
||||
loaded.push((
|
||||
key,
|
||||
ColdEntry {
|
||||
base_key,
|
||||
vary,
|
||||
revision,
|
||||
scope: file.scope,
|
||||
scheme: file.scheme,
|
||||
authority: if file.authority.is_empty() {
|
||||
file.domain.clone()
|
||||
} else {
|
||||
file.authority
|
||||
},
|
||||
domain: file.domain,
|
||||
path: file.path.clone(),
|
||||
target: if file.target.is_empty() {
|
||||
file.path
|
||||
} else {
|
||||
file.target
|
||||
},
|
||||
tags: file.tags,
|
||||
created_at: if file.created_at_ms == 0 {
|
||||
stored_at
|
||||
} else {
|
||||
system_time_ms(file.created_at_ms)
|
||||
},
|
||||
body_path,
|
||||
meta_path: path,
|
||||
body_bytes: file.body_bytes,
|
||||
body_sha256,
|
||||
disk_bytes: body_meta.len().saturating_add(bytes.len() as u64),
|
||||
stored_at,
|
||||
last_access: if file.last_access_ms == 0 {
|
||||
stored_at
|
||||
} else {
|
||||
system_time_ms(file.last_access_ms)
|
||||
},
|
||||
age_at_store_seconds: file.age_at_store_seconds,
|
||||
resident_age_at_store_seconds: file.resident_age_at_store_seconds,
|
||||
ttl: file.ttl,
|
||||
stale_while_revalidate: file.stale_while_revalidate,
|
||||
stale_if_error: file.stale_if_error,
|
||||
must_revalidate: file.must_revalidate,
|
||||
proxy_revalidate: file.proxy_revalidate,
|
||||
has_validator: origin_etag.is_some() || origin_last_modified.is_some(),
|
||||
hits: file.hits,
|
||||
transformed: file.transformed,
|
||||
sequence: 0,
|
||||
generation: 0,
|
||||
skip_compression: file.skip_compression,
|
||||
requires_revalidation: file.requires_revalidation,
|
||||
pinned_reads: 0,
|
||||
},
|
||||
domain: file.domain,
|
||||
path: file.path.clone(),
|
||||
target: if file.target.is_empty() {
|
||||
file.path
|
||||
} else {
|
||||
file.target
|
||||
},
|
||||
tags: file.tags,
|
||||
created_at: if file.created_at_ms == 0 {
|
||||
stored_at
|
||||
} else {
|
||||
system_time_ms(file.created_at_ms)
|
||||
},
|
||||
body_path,
|
||||
meta_path: path,
|
||||
body_bytes: file.body_bytes,
|
||||
body_sha256,
|
||||
disk_bytes: body_meta.len().saturating_add(bytes.len() as u64),
|
||||
stored_at,
|
||||
last_access: if file.last_access_ms == 0 {
|
||||
stored_at
|
||||
} else {
|
||||
system_time_ms(file.last_access_ms)
|
||||
},
|
||||
age_at_store_seconds: file.age_at_store_seconds,
|
||||
resident_age_at_store_seconds: file.resident_age_at_store_seconds,
|
||||
ttl: file.ttl,
|
||||
stale_while_revalidate: file.stale_while_revalidate,
|
||||
stale_if_error: file.stale_if_error,
|
||||
must_revalidate: file.must_revalidate,
|
||||
proxy_revalidate: file.proxy_revalidate,
|
||||
has_validator: origin_etag.is_some() || origin_last_modified.is_some(),
|
||||
hits: file.hits,
|
||||
transformed: file.transformed,
|
||||
sequence: 0,
|
||||
generation: 0,
|
||||
skip_compression: file.skip_compression,
|
||||
requires_revalidation: file.requires_revalidation,
|
||||
pinned_reads: 0,
|
||||
}));
|
||||
));
|
||||
}
|
||||
}
|
||||
loaded.sort_by_key(|(_, entry)| entry.last_access);
|
||||
|
||||
Vendored
+48
-14
@@ -427,7 +427,12 @@ mod dashboard_tests {
|
||||
);
|
||||
let mut headers = HeaderMap::new();
|
||||
settings.annotate_reason_transformed_freshness(
|
||||
&mut headers, "HIT", 3, -7, "cache_max_stale", false,
|
||||
&mut headers,
|
||||
"HIT",
|
||||
3,
|
||||
-7,
|
||||
"cache_max_stale",
|
||||
false,
|
||||
);
|
||||
assert_eq!(
|
||||
headers["cache-status"],
|
||||
@@ -435,7 +440,12 @@ mod dashboard_tests {
|
||||
);
|
||||
let mut headers = HeaderMap::new();
|
||||
settings.annotate_reason_transformed_freshness(
|
||||
&mut headers, "HIT", 3, -5, "cache_stale_while_revalidate", false,
|
||||
&mut headers,
|
||||
"HIT",
|
||||
3,
|
||||
-5,
|
||||
"cache_stale_while_revalidate",
|
||||
false,
|
||||
);
|
||||
assert_eq!(
|
||||
headers["cache-status"],
|
||||
@@ -453,20 +463,38 @@ mod dashboard_tests {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.append("cache-status", HeaderValue::from_static("OriginCache; hit"));
|
||||
settings.annotate_reason_transformed_forwarded(
|
||||
&mut headers, "HIT", 3, 0, "cache_stale_if_error", false,
|
||||
"stale", Some(StatusCode::SERVICE_UNAVAILABLE),
|
||||
&mut headers,
|
||||
"HIT",
|
||||
3,
|
||||
0,
|
||||
"cache_stale_if_error",
|
||||
false,
|
||||
"stale",
|
||||
Some(StatusCode::SERVICE_UNAVAILABLE),
|
||||
);
|
||||
assert_eq!(headers["x-cache"], "HIT");
|
||||
let values: Vec<_> = headers.get_all("cache-status").iter()
|
||||
.map(|value| value.to_str().unwrap()).collect();
|
||||
assert_eq!(values, vec![
|
||||
"OriginCache; hit",
|
||||
"RustProxy; fwd=stale; fwd-status=503; detail=\"cache_stale_if_error\"",
|
||||
]);
|
||||
let values: Vec<_> = headers
|
||||
.get_all("cache-status")
|
||||
.iter()
|
||||
.map(|value| value.to_str().unwrap())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
values,
|
||||
vec![
|
||||
"OriginCache; hit",
|
||||
"RustProxy; fwd=stale; fwd-status=503; detail=\"cache_stale_if_error\"",
|
||||
]
|
||||
);
|
||||
let mut headers = HeaderMap::new();
|
||||
settings.annotate_reason_transformed_forwarded(
|
||||
&mut headers, "HIT", 2, 42, "cache_revalidated", false,
|
||||
"request", Some(StatusCode::NOT_MODIFIED),
|
||||
&mut headers,
|
||||
"HIT",
|
||||
2,
|
||||
42,
|
||||
"cache_revalidated",
|
||||
false,
|
||||
"request",
|
||||
Some(StatusCode::NOT_MODIFIED),
|
||||
);
|
||||
assert_eq!(
|
||||
headers["cache-status"],
|
||||
@@ -474,8 +502,14 @@ mod dashboard_tests {
|
||||
);
|
||||
let mut headers = HeaderMap::new();
|
||||
settings.annotate_reason_transformed_forwarded(
|
||||
&mut headers, "HIT", 2, 0, "cache_stale_if_error", false,
|
||||
"stale", None,
|
||||
&mut headers,
|
||||
"HIT",
|
||||
2,
|
||||
0,
|
||||
"cache_stale_if_error",
|
||||
false,
|
||||
"stale",
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
headers["cache-status"],
|
||||
|
||||
Vendored
+30
-9
@@ -182,7 +182,15 @@ impl Settings {
|
||||
ttl: u64,
|
||||
reason: &'static str,
|
||||
) {
|
||||
self.annotate_internal(headers, outcome, hits, ttl as i64, Some(reason), false, None);
|
||||
self.annotate_internal(
|
||||
headers,
|
||||
outcome,
|
||||
hits,
|
||||
ttl as i64,
|
||||
Some(reason),
|
||||
false,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn annotate_reason_transformed(
|
||||
@@ -194,7 +202,15 @@ impl Settings {
|
||||
reason: &'static str,
|
||||
transformed: bool,
|
||||
) {
|
||||
self.annotate_internal(headers, outcome, hits, ttl as i64, Some(reason), transformed, None);
|
||||
self.annotate_internal(
|
||||
headers,
|
||||
outcome,
|
||||
hits,
|
||||
ttl as i64,
|
||||
Some(reason),
|
||||
transformed,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn annotate_reason_transformed_freshness(
|
||||
@@ -207,12 +223,16 @@ impl Settings {
|
||||
transformed: bool,
|
||||
) {
|
||||
self.annotate_internal(
|
||||
headers, outcome, hits, freshness_ttl, Some(reason), transformed, None,
|
||||
headers,
|
||||
outcome,
|
||||
hits,
|
||||
freshness_ttl,
|
||||
Some(reason),
|
||||
transformed,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
// RFC 9211 distinguishes whether this request was forwarded from whether
|
||||
// the legacy X-Cache metric counted the reused representation as a HIT.
|
||||
pub fn annotate_reason_transformed_forwarded(
|
||||
&self,
|
||||
headers: &mut HeaderMap,
|
||||
@@ -285,8 +305,11 @@ impl Settings {
|
||||
}
|
||||
if self.expose_cache_status {
|
||||
let forwarded = forwarded.or_else(|| {
|
||||
matches!(reason, Some("cache_revalidated" | "cache_revalidated_uncached"))
|
||||
.then_some(("stale", Some(StatusCode::NOT_MODIFIED)))
|
||||
matches!(
|
||||
reason,
|
||||
Some("cache_revalidated" | "cache_revalidated_uncached")
|
||||
)
|
||||
.then_some(("stale", Some(StatusCode::NOT_MODIFIED)))
|
||||
});
|
||||
let mut value = if let Some((why, status)) = forwarded {
|
||||
let mut value = format!("RustProxy; fwd={why}");
|
||||
@@ -296,8 +319,6 @@ impl Settings {
|
||||
value
|
||||
} else {
|
||||
match outcome {
|
||||
// RFC 9211 permits a negative ttl when stale data is served
|
||||
// without sending the current request to the origin.
|
||||
"HIT" => format!("RustProxy; hit; ttl={ttl}"),
|
||||
"MISS" => "RustProxy; fwd=miss".to_owned(),
|
||||
"BYPASS" if reason == Some("request_method") => {
|
||||
|
||||
Vendored
+1
-1
@@ -126,7 +126,7 @@ impl Drop for ColdReadGuard<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// RFC 9211 uses signed structured-field integers for freshness (negative when stale).
|
||||
|
||||
fn cache_status_ttl(ttl: u64, age: u64) -> i64 {
|
||||
const MAX: i128 = 999_999_999_999_999;
|
||||
(i128::from(ttl) - i128::from(age)).clamp(-MAX, MAX) as i64
|
||||
|
||||
Vendored
-2
@@ -87,9 +87,7 @@ pub enum ColdPreset {
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Settings {
|
||||
pub enabled: bool,
|
||||
/// Ignore client freshness directives on cache lookups, like Varnish builtin VCL.
|
||||
pub varnish_compatible: bool,
|
||||
/// Implicit grace in seconds for eligible fresh-at-storage cached responses.
|
||||
pub varnish_grace_seconds: u64,
|
||||
pub memory_preset: Option<MemoryPreset>,
|
||||
pub max_bytes: usize,
|
||||
|
||||
Vendored
+1
-2
@@ -133,8 +133,7 @@ fn stale_mode(
|
||||
}
|
||||
let stale_for = age.saturating_sub(ttl);
|
||||
let max_stale = client_max_stale(request_headers);
|
||||
// An explicit client limit must also constrain implicit Varnish grace.
|
||||
// In particular, max-stale=0 must not serve a stale representation.
|
||||
|
||||
if matches!(max_stale, Some(Some(limit)) if stale_for > limit) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Vendored
+32
-24
@@ -44,9 +44,7 @@ fn directives(headers: &HeaderMap) -> Option<HashMap<String, Option<String>>> {
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
/// Keep all client cache restrictions except the hard-reload directives that
|
||||
/// Varnish normally disregards for a fresh shared object. Never mutate the
|
||||
/// actual request (it is still forwarded to the origin and used for Vary).
|
||||
|
||||
fn compatible_freshness_headers<'a>(
|
||||
headers: &'a HeaderMap,
|
||||
compatible: bool,
|
||||
@@ -54,29 +52,31 @@ fn compatible_freshness_headers<'a>(
|
||||
if !compatible || (!headers.contains_key("cache-control") && !headers.contains_key("pragma")) {
|
||||
return std::borrow::Cow::Borrowed(headers);
|
||||
}
|
||||
// An invalid cache-control header cannot be made valid by filtering it.
|
||||
// Also avoid cloning the full map for ordinary requests with no reload
|
||||
// directives, including min-fresh, max-stale and only-if-cached.
|
||||
|
||||
let Some(control) = directives(headers) else {
|
||||
return std::borrow::Cow::Borrowed(headers);
|
||||
};
|
||||
let remove_reload = control.contains_key("no-cache")
|
||||
|| control.get("max-age").and_then(|value| value.as_deref())
|
||||
.and_then(|value| value.parse::<u64>().ok()) == Some(0);
|
||||
|| control
|
||||
.get("max-age")
|
||||
.and_then(|value| value.as_deref())
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
== Some(0);
|
||||
if !remove_reload && !pragma_no_cache(headers) {
|
||||
return std::borrow::Cow::Borrowed(headers);
|
||||
}
|
||||
let mut freshness = headers.clone();
|
||||
|
||||
let mut changed = false;
|
||||
|
||||
if pragma_no_cache(headers) {
|
||||
// Pragma only affects freshness through its `no-cache` token in this
|
||||
// implementation. Preserve other tokens, if any.
|
||||
freshness.remove("pragma");
|
||||
for header in headers.get_all("pragma") {
|
||||
let Ok(text) = header.to_str() else {
|
||||
return std::borrow::Cow::Borrowed(headers);
|
||||
};
|
||||
let retained = text.split(',')
|
||||
let retained = text
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|token| !token.eq_ignore_ascii_case("no-cache"))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -95,15 +95,21 @@ fn compatible_freshness_headers<'a>(
|
||||
let Ok(text) = header.to_str() else {
|
||||
return std::borrow::Cow::Borrowed(headers);
|
||||
};
|
||||
let retained = text.split(',').map(str::trim).filter(|token| {
|
||||
let (name, value) = token.split_once('=').unwrap_or((token, ""));
|
||||
let name = name.trim();
|
||||
let skip = name.eq_ignore_ascii_case("no-cache")
|
||||
|| (name.eq_ignore_ascii_case("max-age")
|
||||
&& value.trim().trim_matches('"').parse::<u64>() == Ok(0));
|
||||
if skip { changed = true; }
|
||||
!skip
|
||||
}).collect::<Vec<_>>();
|
||||
let retained = text
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|token| {
|
||||
let (name, value) = token.split_once('=').unwrap_or((token, ""));
|
||||
let name = name.trim();
|
||||
let skip = name.eq_ignore_ascii_case("no-cache")
|
||||
|| (name.eq_ignore_ascii_case("max-age")
|
||||
&& value.trim().trim_matches('"').parse::<u64>() == Ok(0));
|
||||
if skip {
|
||||
changed = true;
|
||||
}
|
||||
!skip
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !retained.is_empty() {
|
||||
let Ok(value) = HeaderValue::from_str(&retained.join(", ")) else {
|
||||
return std::borrow::Cow::Borrowed(headers);
|
||||
@@ -118,8 +124,11 @@ fn compatible_freshness_headers<'a>(
|
||||
}
|
||||
}
|
||||
}
|
||||
if changed { std::borrow::Cow::Owned(freshness) }
|
||||
else { std::borrow::Cow::Borrowed(headers) }
|
||||
if changed {
|
||||
std::borrow::Cow::Owned(freshness)
|
||||
} else {
|
||||
std::borrow::Cow::Borrowed(headers)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_freshness_headers<'a>(
|
||||
@@ -243,8 +252,7 @@ pub fn request_policy(
|
||||
return RequestPolicy::Bypass(reason);
|
||||
}
|
||||
}
|
||||
// Compatibility only relaxes browser hard-reload freshness requests.
|
||||
// Retain no-store, only-if-cached, min-fresh and max-stale semantics.
|
||||
|
||||
let Some(control) = directives(headers) else {
|
||||
return RequestPolicy::Bypass("request_cache_control_invalid");
|
||||
};
|
||||
|
||||
Vendored
+17
-18
@@ -127,24 +127,23 @@ pub fn plan(
|
||||
Some(v) => httpdate::parse_http_date(v.to_str().ok()?).ok()?,
|
||||
None => now,
|
||||
};
|
||||
let declared_ttl =
|
||||
if override_public_no_cache {
|
||||
Some(settings.default_ttl_seconds)
|
||||
} else if let Some(value) = control.get("s-maxage").or_else(|| control.get("max-age")) {
|
||||
Some(value.as_ref()?.parse::<u64>().ok()?)
|
||||
} else if let Some(expires) = headers.get("expires") {
|
||||
Some(
|
||||
httpdate::parse_http_date(expires.to_str().ok()?)
|
||||
.ok()?
|
||||
.duration_since(date)
|
||||
.ok()?
|
||||
.as_secs(),
|
||||
)
|
||||
} else if control.contains_key("public") {
|
||||
Some(settings.default_ttl_seconds)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let declared_ttl = if override_public_no_cache {
|
||||
Some(settings.default_ttl_seconds)
|
||||
} else if let Some(value) = control.get("s-maxage").or_else(|| control.get("max-age")) {
|
||||
Some(value.as_ref()?.parse::<u64>().ok()?)
|
||||
} else if let Some(expires) = headers.get("expires") {
|
||||
Some(
|
||||
httpdate::parse_http_date(expires.to_str().ok()?)
|
||||
.ok()?
|
||||
.duration_since(date)
|
||||
.ok()?
|
||||
.as_secs(),
|
||||
)
|
||||
} else if control.contains_key("public") {
|
||||
Some(settings.default_ttl_seconds)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let age = match headers.get("age") {
|
||||
Some(v) => v.to_str().ok()?.parse::<u64>().ok()?,
|
||||
None => 0,
|
||||
|
||||
Vendored
+40
-13
@@ -58,14 +58,29 @@ mod scope_tests {
|
||||
fn stale_snapshot_cannot_issue_cache_ticket_after_config_commit() {
|
||||
let cache = Cache::new(1);
|
||||
cache.set_control_revision(7);
|
||||
let ticket = |revision| cache.ticket_scoped_for_config(
|
||||
revision, 1, "vhost-a", "http", "same.test", "main", "/resource", &HeaderMap::new()
|
||||
);
|
||||
let ticket = |revision| {
|
||||
cache.ticket_scoped_for_config(
|
||||
revision,
|
||||
1,
|
||||
"vhost-a",
|
||||
"http",
|
||||
"same.test",
|
||||
"main",
|
||||
"/resource",
|
||||
&HeaderMap::new(),
|
||||
)
|
||||
};
|
||||
assert!(ticket(7).is_some(), "the current snapshot can cache");
|
||||
cache.set_control_revision(8);
|
||||
assert_eq!(cache.revision(), 1);
|
||||
assert!(ticket(7).is_none(), "late request from the previous snapshot must bypass");
|
||||
assert!(ticket(8).is_some(), "the new snapshot can reuse the same cache identity");
|
||||
assert!(
|
||||
ticket(7).is_none(),
|
||||
"late request from the previous snapshot must bypass"
|
||||
);
|
||||
assert!(
|
||||
ticket(8).is_some(),
|
||||
"the new snapshot can reuse the same cache identity"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -82,18 +97,30 @@ mod scope_tests {
|
||||
assert!(effective.enabled && effective.expose_status);
|
||||
let mut globally_disabled = global.clone();
|
||||
globally_disabled.enabled = false;
|
||||
assert!(!Overrides { enabled: Some(true), ..Overrides::default() }
|
||||
assert!(
|
||||
!Overrides {
|
||||
enabled: Some(true),
|
||||
..Overrides::default()
|
||||
}
|
||||
.resolve(&globally_disabled)
|
||||
.unwrap()
|
||||
.enabled);
|
||||
assert!(!Overrides { enabled: Some(false), ..Overrides::default() }
|
||||
.enabled
|
||||
);
|
||||
assert!(
|
||||
!Overrides {
|
||||
enabled: Some(false),
|
||||
..Overrides::default()
|
||||
}
|
||||
.resolve(&global)
|
||||
.unwrap()
|
||||
.enabled);
|
||||
assert!(!Overrides::default()
|
||||
.resolve(&globally_disabled)
|
||||
.unwrap()
|
||||
.enabled);
|
||||
.enabled
|
||||
);
|
||||
assert!(
|
||||
!Overrides::default()
|
||||
.resolve(&globally_disabled)
|
||||
.unwrap()
|
||||
.enabled
|
||||
);
|
||||
assert_eq!(effective.max_bytes, global.max_bytes);
|
||||
assert_eq!(effective.max_ttl_seconds, 30);
|
||||
assert!(Overrides {
|
||||
|
||||
Vendored
+3
-1
@@ -408,6 +408,8 @@ impl Ticket {
|
||||
}
|
||||
|
||||
fn freshness_headers(&self) -> &HeaderMap {
|
||||
self.freshness_headers.as_ref().unwrap_or(&self.request_headers)
|
||||
self.freshness_headers
|
||||
.as_ref()
|
||||
.unwrap_or(&self.request_headers)
|
||||
}
|
||||
}
|
||||
+551
-85
@@ -4,7 +4,10 @@ mod varnish_compatible_tests {
|
||||
|
||||
fn headers(cache_control: &str) -> HeaderMap {
|
||||
let mut h = HeaderMap::new();
|
||||
h.insert("cache-control", HeaderValue::from_str(cache_control).unwrap());
|
||||
h.insert(
|
||||
"cache-control",
|
||||
HeaderValue::from_str(cache_control).unwrap(),
|
||||
);
|
||||
h
|
||||
}
|
||||
|
||||
@@ -24,7 +27,10 @@ mod varnish_compatible_tests {
|
||||
assert_eq!(customized.varnish_grace_seconds, 17);
|
||||
let mut invalid = customized.clone();
|
||||
invalid.varnish_grace_seconds = 604_801;
|
||||
assert!(invalid.validate().is_err(), "oversized grace must be rejected");
|
||||
assert!(
|
||||
invalid.validate().is_err(),
|
||||
"oversized grace must be rejected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -32,18 +38,58 @@ mod varnish_compatible_tests {
|
||||
let mut compatible = Settings::default();
|
||||
for value in ["no-cache", "max-age=0", "max-age=\"0\"", "max-age=000"] {
|
||||
let h = headers(value);
|
||||
assert_eq!(request_policy(&Method::GET, &h, true, "/style.css", false, &compatible), RequestPolicy::Use);
|
||||
assert_eq!(
|
||||
request_policy(&Method::GET, &h, true, "/style.css", false, &compatible),
|
||||
RequestPolicy::Use
|
||||
);
|
||||
let freshness = request_freshness_headers(&h, &compatible);
|
||||
assert!(!freshness.contains_key("cache-control"), "hard reload must not constrain lookup: {value}");
|
||||
assert!(h.contains_key("cache-control"), "origin request and Vary must remain unchanged");
|
||||
assert!(
|
||||
!freshness.contains_key("cache-control"),
|
||||
"hard reload must not constrain lookup: {value}"
|
||||
);
|
||||
assert!(
|
||||
h.contains_key("cache-control"),
|
||||
"origin request and Vary must remain unchanged"
|
||||
);
|
||||
}
|
||||
for value in ["max-age=1", "min-fresh=600", "max-stale=9999", "only-if-cached"] {
|
||||
for value in [
|
||||
"max-age=1",
|
||||
"min-fresh=600",
|
||||
"max-stale=9999",
|
||||
"only-if-cached",
|
||||
] {
|
||||
let h = headers(value);
|
||||
assert_eq!(request_policy(&Method::GET, &h, true, "/style.css", false, &compatible), RequestPolicy::Use);
|
||||
assert!(request_freshness_headers(&h, &compatible).contains_key("cache-control"), "preserve {value}");
|
||||
assert_eq!(
|
||||
request_policy(&Method::GET, &h, true, "/style.css", false, &compatible),
|
||||
RequestPolicy::Use
|
||||
);
|
||||
assert!(
|
||||
request_freshness_headers(&h, &compatible).contains_key("cache-control"),
|
||||
"preserve {value}"
|
||||
);
|
||||
}
|
||||
assert_eq!(request_policy(&Method::GET, &headers("no-store"), true, "/", false, &compatible), RequestPolicy::Bypass("request_no_store"));
|
||||
assert_eq!(request_policy(&Method::GET, &headers("invalid directive"), true, "/", false, &compatible), RequestPolicy::Bypass("request_cache_control_invalid"));
|
||||
assert_eq!(
|
||||
request_policy(
|
||||
&Method::GET,
|
||||
&headers("no-store"),
|
||||
true,
|
||||
"/",
|
||||
false,
|
||||
&compatible
|
||||
),
|
||||
RequestPolicy::Bypass("request_no_store")
|
||||
);
|
||||
assert_eq!(
|
||||
request_policy(
|
||||
&Method::GET,
|
||||
&headers("invalid directive"),
|
||||
true,
|
||||
"/",
|
||||
false,
|
||||
&compatible
|
||||
),
|
||||
RequestPolicy::Bypass("request_cache_control_invalid")
|
||||
);
|
||||
assert!(only_if_cached(&headers("only-if-cached")));
|
||||
let h = headers("no-cache, max-age=0, min-fresh=45, max-stale=90, only-if-cached");
|
||||
let freshness = request_freshness_headers(&h, &compatible);
|
||||
@@ -55,12 +101,48 @@ mod varnish_compatible_tests {
|
||||
assert!(parsed.contains_key("only-if-cached"));
|
||||
let mut pragma = HeaderMap::new();
|
||||
pragma.insert("pragma", HeaderValue::from_static("no-cache, custom-token"));
|
||||
assert_eq!(request_policy(&Method::GET, &pragma, true, "/", false, &compatible), RequestPolicy::Use);
|
||||
assert_eq!(request_freshness_headers(&pragma, &compatible)["pragma"], "custom-token");
|
||||
assert_eq!(
|
||||
request_policy(&Method::GET, &pragma, true, "/", false, &compatible),
|
||||
RequestPolicy::Use
|
||||
);
|
||||
assert_eq!(
|
||||
request_freshness_headers(&pragma, &compatible)["pragma"],
|
||||
"custom-token"
|
||||
);
|
||||
compatible.varnish_compatible = false;
|
||||
assert_eq!(request_policy(&Method::GET, &headers("no-cache"), true, "/", false, &compatible), RequestPolicy::Revalidate("request_no_cache"));
|
||||
assert_eq!(request_policy(&Method::GET, &headers("max-age=0"), true, "/", false, &compatible), RequestPolicy::Revalidate("request_max_age"));
|
||||
assert_eq!(request_policy(&Method::GET, &headers("no-store"), true, "/", false, &compatible), RequestPolicy::Bypass("request_no_store"));
|
||||
assert_eq!(
|
||||
request_policy(
|
||||
&Method::GET,
|
||||
&headers("no-cache"),
|
||||
true,
|
||||
"/",
|
||||
false,
|
||||
&compatible
|
||||
),
|
||||
RequestPolicy::Revalidate("request_no_cache")
|
||||
);
|
||||
assert_eq!(
|
||||
request_policy(
|
||||
&Method::GET,
|
||||
&headers("max-age=0"),
|
||||
true,
|
||||
"/",
|
||||
false,
|
||||
&compatible
|
||||
),
|
||||
RequestPolicy::Revalidate("request_max_age")
|
||||
);
|
||||
assert_eq!(
|
||||
request_policy(
|
||||
&Method::GET,
|
||||
&headers("no-store"),
|
||||
true,
|
||||
"/",
|
||||
false,
|
||||
&compatible
|
||||
),
|
||||
RequestPolicy::Bypass("request_no_store")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -73,16 +155,55 @@ mod varnish_compatible_tests {
|
||||
("upgrade", "websocket"),
|
||||
] {
|
||||
let mut h = headers("no-cache");
|
||||
h.insert(http::HeaderName::from_bytes(name.as_bytes()).unwrap(), HeaderValue::from_str(value).unwrap());
|
||||
assert!(matches!(request_policy(&Method::GET, &h, true, "/index.html", false, &settings), RequestPolicy::Bypass(_)));
|
||||
h.insert(
|
||||
http::HeaderName::from_bytes(name.as_bytes()).unwrap(),
|
||||
HeaderValue::from_str(value).unwrap(),
|
||||
);
|
||||
assert!(matches!(
|
||||
request_policy(&Method::GET, &h, true, "/index.html", false, &settings),
|
||||
RequestPolicy::Bypass(_)
|
||||
));
|
||||
}
|
||||
settings.bypass_query = true;
|
||||
assert_eq!(request_policy(&Method::GET, &HeaderMap::new(), true, "/style.css", true, &settings), RequestPolicy::Bypass("query_excluded"));
|
||||
assert_eq!(
|
||||
request_policy(
|
||||
&Method::GET,
|
||||
&HeaderMap::new(),
|
||||
true,
|
||||
"/style.css",
|
||||
true,
|
||||
&settings
|
||||
),
|
||||
RequestPolicy::Bypass("query_excluded")
|
||||
);
|
||||
settings.bypass_query = false;
|
||||
settings.bypass_prefixes = vec!["/private".into()];
|
||||
assert_eq!(request_policy(&Method::GET, &HeaderMap::new(), true, "/private/account", false, &settings), RequestPolicy::Bypass("path_excluded"));
|
||||
assert_eq!(request_policy(&Method::POST, &HeaderMap::new(), true, "/style.css", false, &settings), RequestPolicy::Bypass("request_method"));
|
||||
assert!(settings.ignore_cookie_for_path("/style.css"), "existing static cookie feature remains enabled");
|
||||
assert_eq!(
|
||||
request_policy(
|
||||
&Method::GET,
|
||||
&HeaderMap::new(),
|
||||
true,
|
||||
"/private/account",
|
||||
false,
|
||||
&settings
|
||||
),
|
||||
RequestPolicy::Bypass("path_excluded")
|
||||
);
|
||||
assert_eq!(
|
||||
request_policy(
|
||||
&Method::POST,
|
||||
&HeaderMap::new(),
|
||||
true,
|
||||
"/style.css",
|
||||
false,
|
||||
&settings
|
||||
),
|
||||
RequestPolicy::Bypass("request_method")
|
||||
);
|
||||
assert!(
|
||||
settings.ignore_cookie_for_path("/style.css"),
|
||||
"existing static cookie feature remains enabled"
|
||||
);
|
||||
assert!(!settings.ignore_cookie_for_path("/index.html"));
|
||||
}
|
||||
|
||||
@@ -92,7 +213,13 @@ mod varnish_compatible_tests {
|
||||
let mut strict = compatible.clone();
|
||||
strict.varnish_compatible = false;
|
||||
for control in ["private, max-age=60", "no-store, max-age=60"] {
|
||||
assert!(plan(StatusCode::OK, &headers(control), &compatible, Duration::ZERO).is_none());
|
||||
assert!(plan(
|
||||
StatusCode::OK,
|
||||
&headers(control),
|
||||
&compatible,
|
||||
Duration::ZERO
|
||||
)
|
||||
.is_none());
|
||||
assert!(plan(StatusCode::OK, &headers(control), &strict, Duration::ZERO).is_none());
|
||||
}
|
||||
let mut no_cache = headers("no-cache, max-age=60");
|
||||
@@ -101,7 +228,13 @@ mod varnish_compatible_tests {
|
||||
.expect("no-cache with ETag remains revalidatable without fallback");
|
||||
assert!(validator_plan.requires_revalidation);
|
||||
assert!(plan(StatusCode::OK, &no_cache, &strict, Duration::ZERO).is_some());
|
||||
assert!(plan(StatusCode::OK, &headers("no-cache, max-age=60"), &compatible, Duration::ZERO).is_none());
|
||||
assert!(plan(
|
||||
StatusCode::OK,
|
||||
&headers("no-cache, max-age=60"),
|
||||
&compatible,
|
||||
Duration::ZERO
|
||||
)
|
||||
.is_none());
|
||||
|
||||
let mut fallback = compatible.clone();
|
||||
fallback.default_ttl_seconds = 42;
|
||||
@@ -111,21 +244,48 @@ mod varnish_compatible_tests {
|
||||
assert_eq!(fallback_plan.ttl, 42);
|
||||
assert!(!fallback_plan.requires_revalidation);
|
||||
let nonpublic_no_cache = headers("no-cache, max-age=60");
|
||||
assert!(plan(StatusCode::OK, &nonpublic_no_cache, &fallback, Duration::ZERO).is_none());
|
||||
assert!(plan(
|
||||
StatusCode::OK,
|
||||
&nonpublic_no_cache,
|
||||
&fallback,
|
||||
Duration::ZERO
|
||||
)
|
||||
.is_none());
|
||||
let mut surrogate = public_no_cache.clone();
|
||||
surrogate.insert("surrogate-control", HeaderValue::from_static("max-age=120; NO-STORE"));
|
||||
surrogate.insert(
|
||||
"surrogate-control",
|
||||
HeaderValue::from_static("max-age=120; NO-STORE"),
|
||||
);
|
||||
assert!(plan(StatusCode::OK, &surrogate, &fallback, Duration::ZERO).is_none());
|
||||
assert_eq!(response_reason(StatusCode::OK, &surrogate, &fallback), "response_surrogate_control");
|
||||
assert!(plan(StatusCode::OK, &surrogate, &strict, Duration::ZERO).is_none(), "strict no-cache without ETag remains uncached");
|
||||
assert_eq!(
|
||||
response_reason(StatusCode::OK, &surrogate, &fallback),
|
||||
"response_surrogate_control"
|
||||
);
|
||||
assert!(
|
||||
plan(StatusCode::OK, &surrogate, &strict, Duration::ZERO).is_none(),
|
||||
"strict no-cache without ETag remains uncached"
|
||||
);
|
||||
let mut private = headers("public, private, no-cache");
|
||||
private.insert("etag", HeaderValue::from_static("\"secret\""));
|
||||
assert!(plan(StatusCode::OK, &private, &fallback, Duration::ZERO).is_none());
|
||||
|
||||
for control in ["max-age=0", "max-age=60, s-maxage=0", "max-age=0, stale-if-error=120"] {
|
||||
let p = plan(StatusCode::OK, &headers(control), &compatible, Duration::ZERO)
|
||||
.expect("zero TTL is retained for max-stale, validation or stale-if-error");
|
||||
for control in [
|
||||
"max-age=0",
|
||||
"max-age=60, s-maxage=0",
|
||||
"max-age=0, stale-if-error=120",
|
||||
] {
|
||||
let p = plan(
|
||||
StatusCode::OK,
|
||||
&headers(control),
|
||||
&compatible,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.expect("zero TTL is retained for max-stale, validation or stale-if-error");
|
||||
assert_eq!(p.ttl, 0, "{control}");
|
||||
assert_eq!(p.stale_while_revalidate, 0, "zero TTL must not receive implicit grace");
|
||||
assert_eq!(
|
||||
p.stale_while_revalidate, 0,
|
||||
"zero TTL must not receive implicit grace"
|
||||
);
|
||||
}
|
||||
let mut cookie = headers("max-age=60");
|
||||
cookie.insert("set-cookie", HeaderValue::from_static("session=private"));
|
||||
@@ -138,24 +298,66 @@ mod varnish_compatible_tests {
|
||||
#[test]
|
||||
fn explicit_zero_grace_and_stale_if_error_are_preserved() {
|
||||
let compatible = Settings::default();
|
||||
let with_zero = plan(StatusCode::OK, &headers("public, max-age=60, stale-while-revalidate=0"), &compatible, Duration::ZERO).unwrap();
|
||||
let with_zero = plan(
|
||||
StatusCode::OK,
|
||||
&headers("public, max-age=60, stale-while-revalidate=0"),
|
||||
&compatible,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(with_zero.stale_while_revalidate, 0);
|
||||
let with_grace = plan(StatusCode::OK, &headers("public, max-age=60"), &compatible, Duration::ZERO).unwrap();
|
||||
let with_grace = plan(
|
||||
StatusCode::OK,
|
||||
&headers("public, max-age=60"),
|
||||
&compatible,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(with_grace.stale_while_revalidate, 60);
|
||||
let mut custom_settings = compatible.clone();
|
||||
custom_settings.varnish_grace_seconds = 25;
|
||||
let with_custom_default = plan(StatusCode::OK, &headers("public, max-age=60"), &custom_settings, Duration::ZERO).unwrap();
|
||||
let with_custom_default = plan(
|
||||
StatusCode::OK,
|
||||
&headers("public, max-age=60"),
|
||||
&custom_settings,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(with_custom_default.stale_while_revalidate, 25);
|
||||
custom_settings.varnish_grace_seconds = 0;
|
||||
let grace_disabled = plan(StatusCode::OK, &headers("public, max-age=60"), &custom_settings, Duration::ZERO).unwrap();
|
||||
let grace_disabled = plan(
|
||||
StatusCode::OK,
|
||||
&headers("public, max-age=60"),
|
||||
&custom_settings,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(grace_disabled.stale_while_revalidate, 0);
|
||||
custom_settings.varnish_compatible = false;
|
||||
custom_settings.varnish_grace_seconds = 60;
|
||||
let strict = plan(StatusCode::OK, &headers("public, max-age=60"), &custom_settings, Duration::ZERO).unwrap();
|
||||
let strict = plan(
|
||||
StatusCode::OK,
|
||||
&headers("public, max-age=60"),
|
||||
&custom_settings,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(strict.stale_while_revalidate, 0);
|
||||
let with_custom = plan(StatusCode::OK, &headers("public, max-age=60, stale-while-revalidate=30"), &compatible, Duration::ZERO).unwrap();
|
||||
let with_custom = plan(
|
||||
StatusCode::OK,
|
||||
&headers("public, max-age=60, stale-while-revalidate=30"),
|
||||
&compatible,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(with_custom.stale_while_revalidate, 30);
|
||||
let zero = plan(StatusCode::OK, &headers("public, max-age=0, stale-if-error=120"), &compatible, Duration::ZERO).unwrap();
|
||||
let zero = plan(
|
||||
StatusCode::OK,
|
||||
&headers("public, max-age=0, stale-if-error=120"),
|
||||
&compatible,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(zero.ttl, 0);
|
||||
assert_eq!(zero.stale_if_error, 120);
|
||||
assert_eq!(zero.stale_while_revalidate, 0);
|
||||
@@ -165,17 +367,47 @@ mod varnish_compatible_tests {
|
||||
async fn origin_no_cache_etag_can_still_revalidate_without_fallback() {
|
||||
let cache = Cache::new(1);
|
||||
let settings = Settings::default();
|
||||
let ticket = cache.ticket(1, "https", "example.test", "app", "/backend.css", &HeaderMap::new()).unwrap();
|
||||
let ticket = cache
|
||||
.ticket(
|
||||
1,
|
||||
"https",
|
||||
"example.test",
|
||||
"app",
|
||||
"/backend.css",
|
||||
&HeaderMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut response = Response::new(Body::from("etag css"));
|
||||
*response.headers_mut() = headers("public, no-cache, max-age=600");
|
||||
response.headers_mut().insert("etag", HeaderValue::from_static("\"origin-v1\""));
|
||||
let p = plan(StatusCode::OK, response.headers(), &settings, Duration::ZERO).unwrap();
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("etag", HeaderValue::from_static("\"origin-v1\""));
|
||||
let p = plan(
|
||||
StatusCode::OK,
|
||||
response.headers(),
|
||||
&settings,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(p.requires_revalidation);
|
||||
cache.wrap(response, ticket.clone(), p, &settings).into_body().collect().await.unwrap();
|
||||
assert!(cache.get(&ticket).is_none(), "backend no-cache cannot produce an unvalidated HIT");
|
||||
let stored = cache.revalidation(&ticket).expect("validator and body retained for 304");
|
||||
cache
|
||||
.wrap(response, ticket.clone(), p, &settings)
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
cache.get(&ticket).is_none(),
|
||||
"backend no-cache cannot produce an unvalidated HIT"
|
||||
);
|
||||
let stored = cache
|
||||
.revalidation(&ticket)
|
||||
.expect("validator and body retained for 304");
|
||||
let mut request = HeaderMap::new();
|
||||
assert!(stored.apply_validators(&mut request), "conditional request can validate stored object");
|
||||
assert!(
|
||||
stored.apply_validators(&mut request),
|
||||
"conditional request can validate stored object"
|
||||
);
|
||||
assert_eq!(request["if-none-match"], "\"origin-v1\"");
|
||||
}
|
||||
|
||||
@@ -184,14 +416,36 @@ mod varnish_compatible_tests {
|
||||
let cache = Cache::new(1);
|
||||
let mut settings = Settings::default();
|
||||
settings.default_ttl_seconds = 75;
|
||||
let ticket = cache.ticket(1, "https", "example.test", "app", "/backend.css", &HeaderMap::new()).unwrap();
|
||||
let ticket = cache
|
||||
.ticket(
|
||||
1,
|
||||
"https",
|
||||
"example.test",
|
||||
"app",
|
||||
"/backend.css",
|
||||
&HeaderMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut response = Response::new(Body::from("public css"));
|
||||
*response.headers_mut() = headers("public, no-cache");
|
||||
let p = plan(StatusCode::OK, response.headers(), &settings, Duration::ZERO).unwrap();
|
||||
let p = plan(
|
||||
StatusCode::OK,
|
||||
response.headers(),
|
||||
&settings,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(p.ttl, 75);
|
||||
assert!(!p.requires_revalidation);
|
||||
cache.wrap(response, ticket.clone(), p, &settings).into_body().collect().await.unwrap();
|
||||
let hit = cache.get(&ticket).expect("explicit public override allows fresh HIT without ETag");
|
||||
cache
|
||||
.wrap(response, ticket.clone(), p, &settings)
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap();
|
||||
let hit = cache
|
||||
.get(&ticket)
|
||||
.expect("explicit public override allows fresh HIT without ETag");
|
||||
assert!(hit.remaining_ttl > 0);
|
||||
}
|
||||
|
||||
@@ -199,59 +453,189 @@ mod varnish_compatible_tests {
|
||||
async fn zero_ttl_stale_if_error_fallback_survives_in_varnish_mode() {
|
||||
let cache = Cache::new(1);
|
||||
let settings = Settings::default();
|
||||
let ticket = cache.ticket(1, "https", "example.test", "app", "/fallback.css", &HeaderMap::new()).unwrap();
|
||||
let ticket = cache
|
||||
.ticket(
|
||||
1,
|
||||
"https",
|
||||
"example.test",
|
||||
"app",
|
||||
"/fallback.css",
|
||||
&HeaderMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut response = Response::new(Body::from("public fallback"));
|
||||
*response.headers_mut() = headers("public, max-age=0, stale-if-error=120");
|
||||
let p = plan(StatusCode::OK, response.headers(), &settings, Duration::ZERO).unwrap();
|
||||
cache.wrap(response, ticket.clone(), p, &settings).into_body().collect().await.unwrap();
|
||||
assert!(cache.get(&ticket).is_none(), "zero TTL must never create a fresh HIT");
|
||||
assert!(cache.revalidation(&ticket).unwrap().can_stale_if_error(), "body remains available on backend failure");
|
||||
let p = plan(
|
||||
StatusCode::OK,
|
||||
response.headers(),
|
||||
&settings,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
cache
|
||||
.wrap(response, ticket.clone(), p, &settings)
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
cache.get(&ticket).is_none(),
|
||||
"zero TTL must never create a fresh HIT"
|
||||
);
|
||||
assert!(
|
||||
cache.revalidation(&ticket).unwrap().can_stale_if_error(),
|
||||
"body remains available on backend failure"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn min_fresh_and_max_stale_still_constrain_compatibility_hits() {
|
||||
let cache = Cache::new(1);
|
||||
let settings = Settings::default();
|
||||
let ticket = cache.ticket(1, "https", "example.test", "app", "/fresh.css", &HeaderMap::new()).unwrap();
|
||||
let ticket = cache
|
||||
.ticket(
|
||||
1,
|
||||
"https",
|
||||
"example.test",
|
||||
"app",
|
||||
"/fresh.css",
|
||||
&HeaderMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut response = Response::new(Body::from("css"));
|
||||
*response.headers_mut() = headers("public, max-age=60");
|
||||
let p = plan(StatusCode::OK, response.headers(), &settings, Duration::ZERO).unwrap();
|
||||
cache.wrap(response, ticket.clone(), p, &settings).into_body().collect().await.unwrap();
|
||||
let p = plan(
|
||||
StatusCode::OK,
|
||||
response.headers(),
|
||||
&settings,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
cache
|
||||
.wrap(response, ticket.clone(), p, &settings)
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let mut store = cache.store.lock().unwrap();
|
||||
for entry in store.entries.values_mut() {
|
||||
entry.received = Instant::now() - Duration::from_secs(45);
|
||||
}
|
||||
}
|
||||
let too_fresh = cache.ticket(1, "https", "example.test", "app", "/fresh.css", &headers("no-cache, max-age=0, min-fresh=20")).unwrap().with_varnish_compatible(true);
|
||||
assert!(cache.get(&too_fresh).is_none(), "min-fresh requires 20s but only 15s remain");
|
||||
let enough = cache.ticket(1, "https", "example.test", "app", "/fresh.css", &headers("min-fresh=10")).unwrap().with_varnish_compatible(true);
|
||||
assert!(cache.get(&enough).is_some(), "10s remaining freshness is available");
|
||||
let too_fresh = cache
|
||||
.ticket(
|
||||
1,
|
||||
"https",
|
||||
"example.test",
|
||||
"app",
|
||||
"/fresh.css",
|
||||
&headers("no-cache, max-age=0, min-fresh=20"),
|
||||
)
|
||||
.unwrap()
|
||||
.with_varnish_compatible(true);
|
||||
assert!(
|
||||
cache.get(&too_fresh).is_none(),
|
||||
"min-fresh requires 20s but only 15s remain"
|
||||
);
|
||||
let enough = cache
|
||||
.ticket(
|
||||
1,
|
||||
"https",
|
||||
"example.test",
|
||||
"app",
|
||||
"/fresh.css",
|
||||
&headers("min-fresh=10"),
|
||||
)
|
||||
.unwrap()
|
||||
.with_varnish_compatible(true);
|
||||
assert!(
|
||||
cache.get(&enough).is_some(),
|
||||
"10s remaining freshness is available"
|
||||
);
|
||||
{
|
||||
let mut store = cache.store.lock().unwrap();
|
||||
for entry in store.entries.values_mut() {
|
||||
entry.received = Instant::now() - Duration::from_secs(90);
|
||||
}
|
||||
}
|
||||
let max_stale = cache.ticket(1, "https", "example.test", "app", "/fresh.css", &headers("no-cache, max-stale=45")).unwrap().with_varnish_compatible(true);
|
||||
let stale = cache.get(&max_stale).expect("max-stale explicitly allows retained stale object");
|
||||
let max_stale = cache
|
||||
.ticket(
|
||||
1,
|
||||
"https",
|
||||
"example.test",
|
||||
"app",
|
||||
"/fresh.css",
|
||||
&headers("no-cache, max-stale=45"),
|
||||
)
|
||||
.unwrap()
|
||||
.with_varnish_compatible(true);
|
||||
let stale = cache
|
||||
.get(&max_stale)
|
||||
.expect("max-stale explicitly allows retained stale object");
|
||||
assert!(stale.stale);
|
||||
assert!(!stale.background_revalidate, "explicit max-stale never triggers implicit background refresh");
|
||||
let too_old = cache.ticket(1, "https", "example.test", "app", "/fresh.css", &headers("max-stale=10")).unwrap().with_varnish_compatible(true);
|
||||
assert!(
|
||||
!stale.background_revalidate,
|
||||
"explicit max-stale never triggers implicit background refresh"
|
||||
);
|
||||
let too_old = cache
|
||||
.ticket(
|
||||
1,
|
||||
"https",
|
||||
"example.test",
|
||||
"app",
|
||||
"/fresh.css",
|
||||
&headers("max-stale=10"),
|
||||
)
|
||||
.unwrap()
|
||||
.with_varnish_compatible(true);
|
||||
assert!(cache.get(&too_old).is_none());
|
||||
let default = cache.ticket(1, "https", "example.test", "app", "/fresh.css", &HeaderMap::new()).unwrap().with_varnish_compatible(true);
|
||||
assert!(cache.get(&default).is_none(), "expired object cannot be served without stale allowance");
|
||||
let default = cache
|
||||
.ticket(
|
||||
1,
|
||||
"https",
|
||||
"example.test",
|
||||
"app",
|
||||
"/fresh.css",
|
||||
&HeaderMap::new(),
|
||||
)
|
||||
.unwrap()
|
||||
.with_varnish_compatible(true);
|
||||
assert!(
|
||||
cache.get(&default).is_none(),
|
||||
"expired object cannot be served without stale allowance"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn varnish_style_hard_reload_keeps_hot_hit_and_age_without_origin_validator() {
|
||||
let cache = Cache::new(1);
|
||||
let settings = Settings::default();
|
||||
let ticket = cache.ticket(1, "https", "example.test", "app", "/style.css", &HeaderMap::new()).unwrap();
|
||||
let ticket = cache
|
||||
.ticket(
|
||||
1,
|
||||
"https",
|
||||
"example.test",
|
||||
"app",
|
||||
"/style.css",
|
||||
&HeaderMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut response = Response::new(Body::from("public css"));
|
||||
*response.headers_mut() = headers("public, max-age=600");
|
||||
let plan = plan(StatusCode::OK, response.headers(), &settings, Duration::ZERO).unwrap();
|
||||
cache.wrap(response, ticket.clone(), plan, &settings).into_body().collect().await.unwrap();
|
||||
let plan = plan(
|
||||
StatusCode::OK,
|
||||
response.headers(),
|
||||
&settings,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
cache
|
||||
.wrap(response, ticket.clone(), plan, &settings)
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let mut store = cache.store.lock().unwrap();
|
||||
for entry in store.entries.values_mut() {
|
||||
@@ -260,27 +644,68 @@ mod varnish_compatible_tests {
|
||||
}
|
||||
let mut request = headers("no-cache, max-age=0");
|
||||
request.insert("pragma", HeaderValue::from_static("no-cache"));
|
||||
let compatibility_ticket = cache.ticket(1, "https", "example.test", "app", "/style.css", &request).unwrap().with_varnish_compatible(true);
|
||||
let first = cache.get(&compatibility_ticket).expect("hard reload should keep a fresh HIT");
|
||||
assert!(first.response.headers().get("age").unwrap().to_str().unwrap().parse::<u64>().unwrap() >= 45);
|
||||
let compatibility_ticket = cache
|
||||
.ticket(1, "https", "example.test", "app", "/style.css", &request)
|
||||
.unwrap()
|
||||
.with_varnish_compatible(true);
|
||||
let first = cache
|
||||
.get(&compatibility_ticket)
|
||||
.expect("hard reload should keep a fresh HIT");
|
||||
assert!(
|
||||
first
|
||||
.response
|
||||
.headers()
|
||||
.get("age")
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.parse::<u64>()
|
||||
.unwrap()
|
||||
>= 45
|
||||
);
|
||||
assert_eq!(first.hits, 1);
|
||||
let again = cache.get(&compatibility_ticket).unwrap();
|
||||
assert_eq!(again.hits, 2, "hard reload must not reset HIT counter");
|
||||
assert_eq!(cache.view().entries, 1);
|
||||
let strict = cache.ticket(1, "https", "example.test", "app", "/style.css", &request).unwrap();
|
||||
assert!(cache.get(&strict).is_none(), "strict max-age=0 requires revalidation");
|
||||
let strict = cache
|
||||
.ticket(1, "https", "example.test", "app", "/style.css", &request)
|
||||
.unwrap();
|
||||
assert!(
|
||||
cache.get(&strict).is_none(),
|
||||
"strict max-age=0 requires revalidation"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sixty_second_grace_and_no_unconditional_stale_outside_window() {
|
||||
let cache = Cache::new(1);
|
||||
let settings = Settings::default();
|
||||
let ticket = cache.ticket(1, "https", "example.test", "app", "/asset.js", &HeaderMap::new()).unwrap();
|
||||
let ticket = cache
|
||||
.ticket(
|
||||
1,
|
||||
"https",
|
||||
"example.test",
|
||||
"app",
|
||||
"/asset.js",
|
||||
&HeaderMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut response = Response::new(Body::from("js"));
|
||||
*response.headers_mut() = headers("public, max-age=60");
|
||||
let plan = plan(StatusCode::OK, response.headers(), &settings, Duration::ZERO).unwrap();
|
||||
let plan = plan(
|
||||
StatusCode::OK,
|
||||
response.headers(),
|
||||
&settings,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(plan.stale_while_revalidate, 60);
|
||||
cache.wrap(response, ticket.clone(), plan, &settings).into_body().collect().await.unwrap();
|
||||
cache
|
||||
.wrap(response, ticket.clone(), plan, &settings)
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let mut store = cache.store.lock().unwrap();
|
||||
for entry in store.entries.values_mut() {
|
||||
@@ -289,12 +714,50 @@ mod varnish_compatible_tests {
|
||||
}
|
||||
let hit = cache.get(&ticket).expect("grace HIT");
|
||||
assert!(hit.stale && hit.background_revalidate);
|
||||
let max_stale_zero = cache.ticket(1, "https", "example.test", "app", "/asset.js", &headers("max-stale=0")).unwrap().with_varnish_compatible(true);
|
||||
assert!(cache.get(&max_stale_zero).is_none(), "explicit max-stale=0 must override implicit grace");
|
||||
let max_stale_small = cache.ticket(1, "https", "example.test", "app", "/asset.js", &headers("no-cache, max-stale=3")).unwrap().with_varnish_compatible(true);
|
||||
assert!(cache.get(&max_stale_small).is_none(), "max-stale=3 rejects object stale by 5s, even in grace");
|
||||
let max_stale_enough = cache.ticket(1, "https", "example.test", "app", "/asset.js", &headers("no-cache, max-stale=9")).unwrap().with_varnish_compatible(true);
|
||||
let accepted = cache.get(&max_stale_enough).expect("max-stale=9 admits object 5s stale");
|
||||
let max_stale_zero = cache
|
||||
.ticket(
|
||||
1,
|
||||
"https",
|
||||
"example.test",
|
||||
"app",
|
||||
"/asset.js",
|
||||
&headers("max-stale=0"),
|
||||
)
|
||||
.unwrap()
|
||||
.with_varnish_compatible(true);
|
||||
assert!(
|
||||
cache.get(&max_stale_zero).is_none(),
|
||||
"explicit max-stale=0 must override implicit grace"
|
||||
);
|
||||
let max_stale_small = cache
|
||||
.ticket(
|
||||
1,
|
||||
"https",
|
||||
"example.test",
|
||||
"app",
|
||||
"/asset.js",
|
||||
&headers("no-cache, max-stale=3"),
|
||||
)
|
||||
.unwrap()
|
||||
.with_varnish_compatible(true);
|
||||
assert!(
|
||||
cache.get(&max_stale_small).is_none(),
|
||||
"max-stale=3 rejects object stale by 5s, even in grace"
|
||||
);
|
||||
let max_stale_enough = cache
|
||||
.ticket(
|
||||
1,
|
||||
"https",
|
||||
"example.test",
|
||||
"app",
|
||||
"/asset.js",
|
||||
&headers("no-cache, max-stale=9"),
|
||||
)
|
||||
.unwrap()
|
||||
.with_varnish_compatible(true);
|
||||
let accepted = cache
|
||||
.get(&max_stale_enough)
|
||||
.expect("max-stale=9 admits object 5s stale");
|
||||
assert!(accepted.stale);
|
||||
{
|
||||
let mut store = cache.store.lock().unwrap();
|
||||
@@ -302,6 +765,9 @@ mod varnish_compatible_tests {
|
||||
entry.received = Instant::now() - Duration::from_secs(125);
|
||||
}
|
||||
}
|
||||
assert!(cache.get(&ticket).is_none(), "expired beyond grace does not produce HIT");
|
||||
assert!(
|
||||
cache.get(&ticket).is_none(),
|
||||
"expired beyond grace does not produce HIT"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user