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:
gru committed 2026-11-05 18:29:56 +01:00
1 parent d3b492874b
commit eba9f89283
135 files changed
+1425 -1064

No files matched your search

+1 -2
View File
@@ -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,
+44 -1
View File
@@ -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
+2 -2
View File
@@ -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");
+2
View File
@@ -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()),
+2 -4
View File
@@ -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
+7 -8
View File
@@ -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
+1 -2
View File
@@ -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"),
);
}
+18 -17
View File
@@ -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 == "*" {
+1 -1
View File
@@ -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 {
+29 -29
View File
@@ -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();
+13 -20
View File
@@ -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,
};
+9 -12
View File
@@ -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;
+3 -5
View File
@@ -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)