v0.35.1: performance, AGPL licensing and documentation overhaul
- Web Server reads static files inline; proxy middleware chain flattened - About window with license summary and project links; AGPL-3.0-or-later - Rewritten documentation: feature list, screenshots, performance guide - Fix false spikes in per-Vhost request-rate charts
This commit is contained in:
1 parent
d3b492874b
commit
eba9f89283
135 files changed
+1425
-1064
No files matched your search
Vendored
+1
-2
@@ -6,8 +6,7 @@ mod tests {
|
||||
h.insert("cache-control", value.parse().unwrap());
|
||||
h
|
||||
}
|
||||
// Legacy-policy regression tests explicitly opt out of the new 0.34.7
|
||||
// default; varnish-compatible behavior has its own test module.
|
||||
// Legacy policy; varnish-compatible behavior is tested separately.
|
||||
fn settings() -> Settings {
|
||||
Settings {
|
||||
enabled: true,
|
||||
|
||||
Vendored
+44
-1
@@ -1,3 +1,6 @@
|
||||
const UNCACHEABLE_MEMO_TTL: Duration = Duration::from_secs(30);
|
||||
const UNCACHEABLE_MEMO_CAPACITY: usize = 8192;
|
||||
|
||||
struct FillFlight {
|
||||
id: u64,
|
||||
scope: String,
|
||||
@@ -50,7 +53,7 @@ impl Drop for FillLeaderGuard {
|
||||
|
||||
impl Cache {
|
||||
pub async fn claim_fill(self: &Arc<Self>, ticket: &Ticket, settings: &Settings) -> FillClaim {
|
||||
if !settings.collapse_enabled {
|
||||
if !settings.collapse_enabled || self.recently_uncacheable(&ticket.key) {
|
||||
return FillClaim::Disabled;
|
||||
}
|
||||
let existing = {
|
||||
@@ -116,7 +119,46 @@ impl Cache {
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks a key as uncacheable so requests skip coalescing.
|
||||
pub fn remember_uncacheable(&self, ticket: &Ticket) {
|
||||
let now = std::time::Instant::now();
|
||||
let mut memo = self
|
||||
.uncacheable_until
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
if memo.len() >= UNCACHEABLE_MEMO_CAPACITY {
|
||||
memo.retain(|_, until| *until > now);
|
||||
if memo.len() >= UNCACHEABLE_MEMO_CAPACITY {
|
||||
return;
|
||||
}
|
||||
}
|
||||
memo.insert(ticket.key, now + UNCACHEABLE_MEMO_TTL);
|
||||
}
|
||||
|
||||
fn recently_uncacheable(&self, key: &Key) -> bool {
|
||||
let mut memo = self
|
||||
.uncacheable_until
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
match memo.get(key) {
|
||||
Some(until) if *until > std::time::Instant::now() => true,
|
||||
Some(_) => {
|
||||
memo.remove(key);
|
||||
false
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn forget_uncacheable(&self) {
|
||||
self.uncacheable_until
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clear();
|
||||
}
|
||||
|
||||
fn cancel_all_fill_flights(&self) {
|
||||
self.forget_uncacheable();
|
||||
let flights = {
|
||||
let mut flights = self.fill_flights.lock().unwrap_or_else(|e| e.into_inner());
|
||||
flights
|
||||
@@ -130,6 +172,7 @@ impl Cache {
|
||||
}
|
||||
|
||||
fn cancel_scope_fill_flights(&self, scope: &str) {
|
||||
self.forget_uncacheable();
|
||||
let removed = {
|
||||
let mut flights = self.fill_flights.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let keys = flights
|
||||
|
||||
Vendored
+2
-2
@@ -92,9 +92,9 @@ impl Candidate {
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
if !self.headers.contains_key("date") {
|
||||
if !self.headers.contains_key(::http::header::DATE) {
|
||||
if let Ok(value) = httpdate::fmt_http_date(SystemTime::now()).parse() {
|
||||
self.headers.insert("date", value);
|
||||
self.headers.insert(::http::header::DATE, value);
|
||||
}
|
||||
}
|
||||
self.headers.remove("x-request-id");
|
||||
|
||||
Vendored
+2
@@ -23,6 +23,7 @@ impl Cache {
|
||||
cold_read_bytes_active: AtomicU64::new(0),
|
||||
revalidating: Mutex::new(HashSet::new()),
|
||||
fill_flights: Mutex::new(HashMap::new()),
|
||||
uncacheable_until: Mutex::new(HashMap::new()),
|
||||
fill_flight_sequence: AtomicU64::new(0),
|
||||
collapse_waiters: AtomicU64::new(0),
|
||||
bans: Mutex::new(Vec::new()),
|
||||
@@ -75,6 +76,7 @@ impl Cache {
|
||||
cold_read_bytes_active: AtomicU64::new(0),
|
||||
revalidating: Mutex::new(HashSet::new()),
|
||||
fill_flights: Mutex::new(HashMap::new()),
|
||||
uncacheable_until: Mutex::new(HashMap::new()),
|
||||
fill_flight_sequence: AtomicU64::new(0),
|
||||
collapse_waiters: AtomicU64::new(0),
|
||||
bans: Mutex::new(Vec::new()),
|
||||
|
||||
Vendored
+2
-4
@@ -87,8 +87,7 @@ impl Cache {
|
||||
if skip_compression {
|
||||
response.extensions_mut().insert(crate::compression::Skip);
|
||||
}
|
||||
response.headers_mut().insert(
|
||||
"age",
|
||||
response.headers_mut().insert(::http::header::AGE,
|
||||
HeaderValue::from_str(&age.to_string()).expect("integer header"),
|
||||
);
|
||||
self.domain_stats
|
||||
@@ -285,8 +284,7 @@ impl Cache {
|
||||
if entry.skip_compression {
|
||||
response.extensions_mut().insert(crate::compression::Skip);
|
||||
}
|
||||
response.headers_mut().insert(
|
||||
"age",
|
||||
response.headers_mut().insert(::http::header::AGE,
|
||||
HeaderValue::from_str(&age.to_string()).expect("integer header"),
|
||||
);
|
||||
self.domain_stats
|
||||
|
||||
Vendored
+7
-8
@@ -22,9 +22,9 @@ impl Cache {
|
||||
if cached.body.len() > settings.max_object_bytes {
|
||||
return None;
|
||||
}
|
||||
if !headers.contains_key("date") {
|
||||
if !headers.contains_key(::http::header::DATE) {
|
||||
if let Ok(value) = httpdate::fmt_http_date(SystemTime::now()).parse() {
|
||||
headers.insert("date", value);
|
||||
headers.insert(::http::header::DATE, value);
|
||||
}
|
||||
}
|
||||
headers.remove("x-request-id");
|
||||
@@ -34,11 +34,11 @@ impl Cache {
|
||||
let vary = plan.vary.clone();
|
||||
let tags = cache_tags(&headers);
|
||||
let origin_etag = validation_headers
|
||||
.get("etag")
|
||||
.get(::http::header::ETAG)
|
||||
.cloned()
|
||||
.or_else(|| cached.origin_etag.clone());
|
||||
let origin_last_modified = validation_headers
|
||||
.get("last-modified")
|
||||
.get(::http::header::LAST_MODIFIED)
|
||||
.cloned()
|
||||
.or_else(|| cached.origin_last_modified.clone());
|
||||
let cost = hot_entry_cost(
|
||||
@@ -98,8 +98,7 @@ impl Cache {
|
||||
}
|
||||
self.counters.hits.fetch_add(1, Ordering::Relaxed);
|
||||
let mut response = cached.response(headers);
|
||||
response.headers_mut().insert(
|
||||
"age",
|
||||
response.headers_mut().insert(::http::header::AGE,
|
||||
HeaderValue::from_str(&age.to_string()).expect("integer header"),
|
||||
);
|
||||
Some(Hit {
|
||||
@@ -156,11 +155,11 @@ impl Cache {
|
||||
.cloned();
|
||||
let origin_etag = match origin_validators.as_ref() {
|
||||
Some(validators) => validators.etag.clone(),
|
||||
None => headers.get("etag").cloned(),
|
||||
None => headers.get(::http::header::ETAG).cloned(),
|
||||
};
|
||||
let origin_last_modified = match origin_validators.as_ref() {
|
||||
Some(validators) => validators.last_modified.clone(),
|
||||
None => headers.get("last-modified").cloned(),
|
||||
None => headers.get(::http::header::LAST_MODIFIED).cloned(),
|
||||
};
|
||||
let transformed = parts
|
||||
.extensions
|
||||
|
||||
Vendored
+1
-2
@@ -333,8 +333,7 @@ impl Settings {
|
||||
if transformed {
|
||||
value.push_str("; transformed");
|
||||
}
|
||||
headers.append(
|
||||
"cache-status",
|
||||
headers.append(::http::header::CACHE_STATUS,
|
||||
HeaderValue::from_str(&value).expect("generated Cache-Status member"),
|
||||
);
|
||||
}
|
||||
|
||||
Vendored
+18
-17
@@ -68,6 +68,7 @@ pub struct Cache {
|
||||
cold_read_bytes_active: AtomicU64,
|
||||
revalidating: Mutex<HashSet<Key>>,
|
||||
fill_flights: Mutex<HashMap<Key, Arc<FillFlight>>>,
|
||||
uncacheable_until: Mutex<HashMap<Key, std::time::Instant>>,
|
||||
fill_flight_sequence: AtomicU64,
|
||||
collapse_waiters: AtomicU64,
|
||||
bans: Mutex<Vec<BanRule>>,
|
||||
@@ -183,35 +184,35 @@ impl Revalidation {
|
||||
&self.headers
|
||||
}
|
||||
pub fn apply_validators(&self, headers: &mut HeaderMap) -> bool {
|
||||
if headers.contains_key("if-none-match") {
|
||||
let Some(public_etag) = self.headers.get("etag").and_then(|v| v.to_str().ok()) else {
|
||||
if headers.contains_key(::http::header::IF_NONE_MATCH) {
|
||||
let Some(public_etag) = self.headers.get(::http::header::ETAG).and_then(|v| v.to_str().ok()) else {
|
||||
return false;
|
||||
};
|
||||
if !single_if_none_match_matches(headers, public_etag) {
|
||||
return false;
|
||||
}
|
||||
if let Some(origin) = self.origin_etag.clone() {
|
||||
headers.insert("if-none-match", origin);
|
||||
headers.remove("if-modified-since");
|
||||
headers.insert(::http::header::IF_NONE_MATCH, origin);
|
||||
headers.remove(::http::header::IF_MODIFIED_SINCE);
|
||||
return true;
|
||||
}
|
||||
if let Some(modified) = self.origin_last_modified.clone() {
|
||||
headers.remove("if-none-match");
|
||||
headers.insert("if-modified-since", modified);
|
||||
headers.remove(::http::header::IF_NONE_MATCH);
|
||||
headers.insert(::http::header::IF_MODIFIED_SINCE, modified);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if headers.contains_key("if-modified-since") {
|
||||
if headers.contains_key(::http::header::IF_MODIFIED_SINCE) {
|
||||
let Some(cached) = self
|
||||
.headers
|
||||
.get("last-modified")
|
||||
.get(::http::header::LAST_MODIFIED)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(client) = headers
|
||||
.get("if-modified-since")
|
||||
.get(::http::header::IF_MODIFIED_SINCE)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
else {
|
||||
return false;
|
||||
@@ -226,16 +227,16 @@ impl Revalidation {
|
||||
let Some(origin) = self.origin_last_modified.clone() else {
|
||||
return false;
|
||||
};
|
||||
headers.insert("if-modified-since", origin);
|
||||
headers.insert(::http::header::IF_MODIFIED_SINCE, origin);
|
||||
return true;
|
||||
}
|
||||
if let Some(etag) = self.origin_etag.clone() {
|
||||
headers.insert("if-none-match", etag);
|
||||
headers.remove("if-modified-since");
|
||||
headers.insert(::http::header::IF_NONE_MATCH, etag);
|
||||
headers.remove(::http::header::IF_MODIFIED_SINCE);
|
||||
true
|
||||
} else if let Some(modified) = self.origin_last_modified.clone() {
|
||||
headers.insert("if-modified-since", modified);
|
||||
headers.remove("if-none-match");
|
||||
headers.insert(::http::header::IF_MODIFIED_SINCE, modified);
|
||||
headers.remove(::http::header::IF_NONE_MATCH);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -281,7 +282,7 @@ impl Revalidation {
|
||||
}
|
||||
pub fn merge_validation_headers(&self, validation: &HeaderMap) -> HeaderMap {
|
||||
let mut merged = merge_revalidated_headers(&self.headers, validation);
|
||||
let public_etag = self.headers.get("etag").cloned();
|
||||
let public_etag = self.headers.get(::http::header::ETAG).cloned();
|
||||
let representation_etag_differs = public_etag.as_ref().is_some_and(|public| {
|
||||
self.origin_etag
|
||||
.as_ref()
|
||||
@@ -289,7 +290,7 @@ impl Revalidation {
|
||||
});
|
||||
if representation_etag_differs {
|
||||
if let Some(public) = public_etag {
|
||||
merged.insert("etag", public);
|
||||
merged.insert(::http::header::ETAG, public);
|
||||
}
|
||||
}
|
||||
merged
|
||||
@@ -316,7 +317,7 @@ impl Revalidation {
|
||||
}
|
||||
fn vary_names(headers: &HeaderMap) -> Option<Vec<String>> {
|
||||
let mut names = Vec::new();
|
||||
for value in headers.get_all("vary") {
|
||||
for value in headers.get_all(::http::header::VARY) {
|
||||
for raw in value.to_str().ok()?.split(',') {
|
||||
let raw = raw.trim();
|
||||
if raw.is_empty() || raw == "*" {
|
||||
|
||||
Vendored
+1
-1
@@ -77,7 +77,7 @@ fn retention_deadline(
|
||||
}
|
||||
|
||||
fn has_validator(headers: &HeaderMap) -> bool {
|
||||
headers.contains_key("etag") || headers.contains_key("last-modified")
|
||||
headers.contains_key(::http::header::ETAG) || headers.contains_key(::http::header::LAST_MODIFIED)
|
||||
}
|
||||
|
||||
fn stale_forbidden(must_revalidate: bool, proxy_revalidate: bool) -> bool {
|
||||
|
||||
Vendored
+29
-29
@@ -1,6 +1,6 @@
|
||||
fn directives(headers: &HeaderMap) -> Option<HashMap<String, Option<String>>> {
|
||||
let mut result = HashMap::new();
|
||||
for header in headers.get_all("cache-control") {
|
||||
for header in headers.get_all(::http::header::CACHE_CONTROL) {
|
||||
for raw in header.to_str().ok()?.split(',') {
|
||||
let raw = raw.trim();
|
||||
if raw.is_empty() {
|
||||
@@ -49,7 +49,7 @@ fn compatible_freshness_headers<'a>(
|
||||
headers: &'a HeaderMap,
|
||||
compatible: bool,
|
||||
) -> std::borrow::Cow<'a, HeaderMap> {
|
||||
if !compatible || (!headers.contains_key("cache-control") && !headers.contains_key("pragma")) {
|
||||
if !compatible || (!headers.contains_key(::http::header::CACHE_CONTROL) && !headers.contains_key(::http::header::PRAGMA)) {
|
||||
return std::borrow::Cow::Borrowed(headers);
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ fn compatible_freshness_headers<'a>(
|
||||
let mut changed = false;
|
||||
|
||||
if pragma_no_cache(headers) {
|
||||
freshness.remove("pragma");
|
||||
for header in headers.get_all("pragma") {
|
||||
freshness.remove(::http::header::PRAGMA);
|
||||
for header in headers.get_all(::http::header::PRAGMA) {
|
||||
let Ok(text) = header.to_str() else {
|
||||
return std::borrow::Cow::Borrowed(headers);
|
||||
};
|
||||
@@ -84,14 +84,14 @@ fn compatible_freshness_headers<'a>(
|
||||
let Ok(value) = HeaderValue::from_str(&retained.join(", ")) else {
|
||||
return std::borrow::Cow::Borrowed(headers);
|
||||
};
|
||||
freshness.append("pragma", value);
|
||||
freshness.append(::http::header::PRAGMA, value);
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
if headers.contains_key("cache-control") {
|
||||
if headers.contains_key(::http::header::CACHE_CONTROL) {
|
||||
let mut retained_values = Vec::new();
|
||||
for header in headers.get_all("cache-control") {
|
||||
for header in headers.get_all(::http::header::CACHE_CONTROL) {
|
||||
let Ok(text) = header.to_str() else {
|
||||
return std::borrow::Cow::Borrowed(headers);
|
||||
};
|
||||
@@ -118,9 +118,9 @@ fn compatible_freshness_headers<'a>(
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
freshness.remove("cache-control");
|
||||
freshness.remove(::http::header::CACHE_CONTROL);
|
||||
for value in retained_values {
|
||||
freshness.append("cache-control", value);
|
||||
freshness.append(::http::header::CACHE_CONTROL, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ pub fn request_freshness_headers<'a>(
|
||||
|
||||
pub fn only_if_cached(headers: &HeaderMap) -> bool {
|
||||
headers
|
||||
.get_all("cache-control")
|
||||
.get_all(::http::header::CACHE_CONTROL)
|
||||
.iter()
|
||||
.filter_map(|v| v.to_str().ok())
|
||||
.any(|v| {
|
||||
@@ -170,7 +170,7 @@ impl RequestPolicy {
|
||||
}
|
||||
fn pragma_no_cache(headers: &HeaderMap) -> bool {
|
||||
headers
|
||||
.get_all("pragma")
|
||||
.get_all(::http::header::PRAGMA)
|
||||
.iter()
|
||||
.filter_map(|value| value.to_str().ok())
|
||||
.any(|value| {
|
||||
@@ -193,7 +193,7 @@ pub fn forced_request_policy(
|
||||
if matches!(request_range(method, headers), RangeSelection::Unsupported) {
|
||||
return RequestPolicy::Bypass("request_range_unsupported");
|
||||
}
|
||||
if headers.contains_key("if-range") && !headers.contains_key("range") {
|
||||
if headers.contains_key(::http::header::IF_RANGE) && !headers.contains_key(::http::header::RANGE) {
|
||||
return RequestPolicy::Bypass("request_precondition");
|
||||
}
|
||||
for key in ["if-match", "if-unmodified-since", "upgrade"] {
|
||||
@@ -225,7 +225,7 @@ pub fn request_policy(
|
||||
if matches!(request_range(method, headers), RangeSelection::Unsupported) {
|
||||
return RequestPolicy::Bypass("request_range_unsupported");
|
||||
}
|
||||
if headers.contains_key("if-range") && !headers.contains_key("range") {
|
||||
if headers.contains_key(::http::header::IF_RANGE) && !headers.contains_key(::http::header::RANGE) {
|
||||
return RequestPolicy::Bypass("request_precondition");
|
||||
}
|
||||
if settings.bypass_query && has_query {
|
||||
@@ -285,7 +285,7 @@ pub fn request_policy(
|
||||
}
|
||||
}
|
||||
if !settings.varnish_compatible
|
||||
&& !headers.contains_key("cache-control")
|
||||
&& !headers.contains_key(::http::header::CACHE_CONTROL)
|
||||
&& pragma_no_cache(headers)
|
||||
{
|
||||
return RequestPolicy::Revalidate("request_pragma_no_cache");
|
||||
@@ -327,7 +327,7 @@ fn weak_etag(value: &str) -> Option<&str> {
|
||||
}
|
||||
fn if_none_match_matches(headers: &HeaderMap, etag: &str) -> Option<bool> {
|
||||
let target = weak_etag(etag)?;
|
||||
let values: Vec<_> = headers.get_all("if-none-match").iter().collect();
|
||||
let values: Vec<_> = headers.get_all(::http::header::IF_NONE_MATCH).iter().collect();
|
||||
if values.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -362,7 +362,7 @@ fn single_if_none_match_matches(headers: &HeaderMap, etag: &str) -> bool {
|
||||
let Some(target) = weak_etag(etag) else {
|
||||
return false;
|
||||
};
|
||||
let values: Vec<_> = headers.get_all("if-none-match").iter().collect();
|
||||
let values: Vec<_> = headers.get_all(::http::header::IF_NONE_MATCH).iter().collect();
|
||||
if values.len() != 1 {
|
||||
return false;
|
||||
}
|
||||
@@ -381,22 +381,22 @@ fn single_if_none_match_matches(headers: &HeaderMap, etag: &str) -> bool {
|
||||
&& weak_etag(token).is_some_and(|candidate| candidate == target)
|
||||
}
|
||||
pub fn conditional_not_modified(request: &HeaderMap, response: &HeaderMap) -> bool {
|
||||
if request.contains_key("if-none-match") {
|
||||
if request.contains_key(::http::header::IF_NONE_MATCH) {
|
||||
return response
|
||||
.get("etag")
|
||||
.get(::http::header::ETAG)
|
||||
.and_then(|etag| etag.to_str().ok())
|
||||
.and_then(|etag| if_none_match_matches(request, etag))
|
||||
.unwrap_or(false);
|
||||
}
|
||||
let Some(since) = request
|
||||
.get("if-modified-since")
|
||||
.get(::http::header::IF_MODIFIED_SINCE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| httpdate::parse_http_date(value).ok())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
response
|
||||
.get("last-modified")
|
||||
.get(::http::header::LAST_MODIFIED)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| httpdate::parse_http_date(value).ok())
|
||||
.is_some_and(|modified| modified <= since)
|
||||
@@ -411,8 +411,8 @@ pub fn make_not_modified(mut response: Response) -> Response {
|
||||
}
|
||||
pub fn merge_revalidated_headers(cached: &HeaderMap, validation: &HeaderMap) -> HeaderMap {
|
||||
let mut merged = cached.clone();
|
||||
if !validation.contains_key("age") {
|
||||
merged.remove("age");
|
||||
if !validation.contains_key(::http::header::AGE) {
|
||||
merged.remove(::http::header::AGE);
|
||||
}
|
||||
for name in validation.keys() {
|
||||
if matches!(
|
||||
@@ -426,16 +426,16 @@ pub fn merge_revalidated_headers(cached: &HeaderMap, validation: &HeaderMap) ->
|
||||
merged.append(name.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
if !validation.contains_key("date") {
|
||||
if !validation.contains_key(::http::header::DATE) {
|
||||
if let Ok(value) = httpdate::fmt_http_date(SystemTime::now()).parse() {
|
||||
merged.insert("date", value);
|
||||
merged.insert(::http::header::DATE, value);
|
||||
}
|
||||
}
|
||||
merged
|
||||
}
|
||||
|
||||
pub fn varies_on(headers: &HeaderMap, wanted: &str) -> bool {
|
||||
headers.get_all("vary").iter().any(|value| {
|
||||
headers.get_all(::http::header::VARY).iter().any(|value| {
|
||||
value.to_str().ok().is_some_and(|value| {
|
||||
value
|
||||
.split(',')
|
||||
@@ -465,13 +465,13 @@ pub fn response_reason(
|
||||
if status != StatusCode::OK {
|
||||
return "response_status";
|
||||
}
|
||||
if headers.contains_key("set-cookie") {
|
||||
if headers.contains_key(::http::header::SET_COOKIE) {
|
||||
return "response_set_cookie";
|
||||
}
|
||||
if headers.contains_key("content-range") {
|
||||
if headers.contains_key(::http::header::CONTENT_RANGE) {
|
||||
return "response_range";
|
||||
}
|
||||
if headers.contains_key("trailer") {
|
||||
if headers.contains_key(::http::header::TRAILER) {
|
||||
return "response_trailers";
|
||||
}
|
||||
let Some(control) = directives(headers) else {
|
||||
@@ -485,7 +485,7 @@ pub fn response_reason(
|
||||
if settings.varnish_compatible && surrogate_no_store(headers) {
|
||||
return "response_surrogate_control";
|
||||
}
|
||||
for value in headers.get_all("vary") {
|
||||
for value in headers.get_all(::http::header::VARY) {
|
||||
if value.to_str().ok().is_none_or(|value| {
|
||||
value.split(',').any(|name| {
|
||||
let name = name.trim();
|
||||
|
||||
Vendored
+13
-20
@@ -61,9 +61,9 @@ pub fn plan(
|
||||
return None;
|
||||
}
|
||||
if status != StatusCode::OK
|
||||
|| headers.contains_key("set-cookie")
|
||||
|| headers.contains_key("content-range")
|
||||
|| headers.contains_key("trailer")
|
||||
|| headers.contains_key(::http::header::SET_COOKIE)
|
||||
|| headers.contains_key(::http::header::CONTENT_RANGE)
|
||||
|| headers.contains_key(::http::header::TRAILER)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -75,7 +75,7 @@ pub fn plan(
|
||||
return None;
|
||||
}
|
||||
if headers
|
||||
.get("content-type")
|
||||
.get(::http::header::CONTENT_TYPE)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.is_some_and(|v| {
|
||||
let v = v.trim_start().to_ascii_lowercase();
|
||||
@@ -92,14 +92,11 @@ pub fn plan(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// Surrogate-Control: no-store is a shared-cache veto in compatibility
|
||||
// mode, regardless of public/fallback TTL or any other directive.
|
||||
// Surrogate-Control: no-store always vetoes shared caching.
|
||||
if settings.varnish_compatible && surrogate_no_store(headers) {
|
||||
return None;
|
||||
}
|
||||
// Explicitly configured fallback TTL is an opt-in origin no-cache
|
||||
// override *only* for public responses. With fallback disabled, keep
|
||||
// the pre-0.34.7 validator-based revalidation and never grant a HIT.
|
||||
// Fallback TTL overrides origin no-cache only for public responses.
|
||||
let override_public_no_cache = settings.varnish_compatible
|
||||
&& control.contains_key("no-cache")
|
||||
&& control.contains_key("public")
|
||||
@@ -117,13 +114,13 @@ pub fn plan(
|
||||
let mut stale_while_revalidate = parse_window("stale-while-revalidate")?;
|
||||
let stale_if_error = parse_window("stale-if-error")?;
|
||||
if requires_revalidation
|
||||
&& !headers.contains_key("etag")
|
||||
&& !headers.contains_key("last-modified")
|
||||
&& !headers.contains_key(::http::header::ETAG)
|
||||
&& !headers.contains_key(::http::header::LAST_MODIFIED)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let now = SystemTime::now();
|
||||
let date = match headers.get("date") {
|
||||
let date = match headers.get(::http::header::DATE) {
|
||||
Some(v) => httpdate::parse_http_date(v.to_str().ok()?).ok()?,
|
||||
None => now,
|
||||
};
|
||||
@@ -131,7 +128,7 @@ pub fn plan(
|
||||
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") {
|
||||
} else if let Some(expires) = headers.get(::http::header::EXPIRES) {
|
||||
Some(
|
||||
httpdate::parse_http_date(expires.to_str().ok()?)
|
||||
.ok()?
|
||||
@@ -144,7 +141,7 @@ pub fn plan(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let age = match headers.get("age") {
|
||||
let age = match headers.get(::http::header::AGE) {
|
||||
Some(v) => v.to_str().ok()?.parse::<u64>().ok()?,
|
||||
None => 0,
|
||||
};
|
||||
@@ -161,11 +158,7 @@ pub fn plan(
|
||||
None if requires_revalidation => 0,
|
||||
None => return None,
|
||||
};
|
||||
// Retain zero-TTL and already-expired representations for conditional
|
||||
// revalidation, client max-stale or stale-if-error. They must not be
|
||||
// served as fresh HITs. Varnish-style grace is only implicit when the
|
||||
// origin did not explicitly set stale-while-revalidate (including =0),
|
||||
// the object arrived fresh and no origin no-cache rule is in effect.
|
||||
// Expired entries are kept for revalidation only; implicit grace needs no explicit SWR/no-cache.
|
||||
if settings.varnish_compatible
|
||||
&& !control.contains_key("stale-while-revalidate")
|
||||
&& !control.contains_key("no-cache")
|
||||
@@ -184,7 +177,7 @@ pub fn plan(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let expected_length = match headers.get("content-length") {
|
||||
let expected_length = match headers.get(::http::header::CONTENT_LENGTH) {
|
||||
Some(v) => Some(v.to_str().ok()?.parse::<usize>().ok()?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Vendored
+9
-12
@@ -12,11 +12,11 @@ pub enum RangeSelection {
|
||||
}
|
||||
|
||||
pub fn request_range(method: &Method, headers: &HeaderMap) -> RangeSelection {
|
||||
let mut values = headers.get_all("range").iter();
|
||||
let mut values = headers.get_all(::http::header::RANGE).iter();
|
||||
let Some(value) = values.next() else {
|
||||
return RangeSelection::None;
|
||||
};
|
||||
if values.next().is_some() || headers.get_all("if-range").iter().count() > 1 {
|
||||
if values.next().is_some() || headers.get_all(::http::header::IF_RANGE).iter().count() > 1 {
|
||||
return RangeSelection::Unsupported;
|
||||
}
|
||||
if method != Method::GET {
|
||||
@@ -74,7 +74,7 @@ fn if_range_matches(headers: &HeaderMap, if_range: Option<&HeaderValue>) -> bool
|
||||
return false;
|
||||
}
|
||||
return headers
|
||||
.get("etag")
|
||||
.get(::http::header::ETAG)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|etag| !etag.trim().starts_with("W/") && etag.trim() == candidate);
|
||||
}
|
||||
@@ -82,7 +82,7 @@ fn if_range_matches(headers: &HeaderMap, if_range: Option<&HeaderValue>) -> bool
|
||||
return false;
|
||||
};
|
||||
headers
|
||||
.get("last-modified")
|
||||
.get(::http::header::LAST_MODIFIED)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| httpdate::parse_http_date(value).ok())
|
||||
.is_some_and(|modified| modified <= candidate_date)
|
||||
@@ -127,32 +127,29 @@ impl Hit {
|
||||
let full_len = self.body.len();
|
||||
self.response
|
||||
.headers_mut()
|
||||
.insert("accept-ranges", HeaderValue::from_static("bytes"));
|
||||
.insert(::http::header::ACCEPT_RANGES, HeaderValue::from_static("bytes"));
|
||||
self.response
|
||||
.extensions_mut()
|
||||
.insert(crate::compression::Skip);
|
||||
let Some((start, end)) = resolve_byte_range(range, full_len) else {
|
||||
*self.response.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
|
||||
*self.response.body_mut() = Body::empty();
|
||||
self.response.headers_mut().insert(
|
||||
"content-range",
|
||||
self.response.headers_mut().insert(::http::header::CONTENT_RANGE,
|
||||
HeaderValue::from_str(&format!("bytes */{full_len}")).expect("generated range"),
|
||||
);
|
||||
self.response
|
||||
.headers_mut()
|
||||
.insert("content-length", HeaderValue::from_static("0"));
|
||||
.insert(::http::header::CONTENT_LENGTH, HeaderValue::from_static("0"));
|
||||
return self;
|
||||
};
|
||||
let slice = self.body.slice(start..=end);
|
||||
*self.response.status_mut() = StatusCode::PARTIAL_CONTENT;
|
||||
*self.response.body_mut() = Body::from(slice.clone());
|
||||
self.response.headers_mut().insert(
|
||||
"content-range",
|
||||
self.response.headers_mut().insert(::http::header::CONTENT_RANGE,
|
||||
HeaderValue::from_str(&format!("bytes {start}-{end}/{full_len}"))
|
||||
.expect("generated range"),
|
||||
);
|
||||
self.response.headers_mut().insert(
|
||||
"content-length",
|
||||
self.response.headers_mut().insert(::http::header::CONTENT_LENGTH,
|
||||
HeaderValue::from_str(&slice.len().to_string()).expect("generated length"),
|
||||
);
|
||||
self.body = slice;
|
||||
|
||||
Vendored
+3
-5
@@ -294,9 +294,8 @@ impl Cache {
|
||||
}
|
||||
let hits = cached.hits.saturating_add(1);
|
||||
let mut headers = cached.headers.clone();
|
||||
headers.insert("age", HeaderValue::from_str(&cached.age.to_string()).ok()?);
|
||||
headers.append(
|
||||
"warning",
|
||||
headers.insert(::http::header::AGE, HeaderValue::from_str(&cached.age.to_string()).ok()?);
|
||||
headers.append(::http::header::WARNING,
|
||||
HeaderValue::from_static("110 RustProxy \"Response is stale\""),
|
||||
);
|
||||
let response = cached.response(headers);
|
||||
@@ -396,8 +395,7 @@ impl Cache {
|
||||
}
|
||||
|
||||
impl Ticket {
|
||||
/// Ignore only browser forced reload for freshness; keep min-fresh,
|
||||
/// max-stale and other client constraints, plus original Vary headers.
|
||||
/// Ignores forced-reload directives only; other client constraints apply.
|
||||
pub fn with_varnish_compatible(mut self, compatible: bool) -> Self {
|
||||
if let std::borrow::Cow::Owned(freshness) =
|
||||
compatible_freshness_headers(&self.request_headers, compatible)
|
||||
|
||||
Reference in new issue
Block a user