impl Plan { pub fn remaining_ttl(&self) -> u64 { self.ttl.saturating_sub(self.initial_age.as_secs()) } pub fn with_vary(mut self, name: &str) -> Self { if !self .vary .iter() .any(|value| value.eq_ignore_ascii_case(name)) { self.vary.push(name.to_ascii_lowercase()); self.vary.sort(); } self } pub fn without_vary(mut self, name: &str) -> Self { self.vary.retain(|value| !value.eq_ignore_ascii_case(name)); self } pub fn restrict(mut self, transformed: Self) -> Self { self.ttl = self.ttl.min(transformed.ttl); self.stale_while_revalidate = self .stale_while_revalidate .min(transformed.stale_while_revalidate); self.stale_if_error = self.stale_if_error.min(transformed.stale_if_error); self.must_revalidate |= transformed.must_revalidate; self.proxy_revalidate |= transformed.proxy_revalidate; self.requires_revalidation |= transformed.requires_revalidation; self.vary.extend(transformed.vary); self.vary.sort(); self.vary.dedup(); self.expected_length = transformed.expected_length; if transformed.received >= self.received { let carried = self .initial_age .saturating_add(transformed.received.duration_since(self.received)); self.received = transformed.received; self.initial_age = carried.max(transformed.initial_age); } else { let carried = transformed .initial_age .saturating_add(self.received.duration_since(transformed.received)); self.initial_age = self.initial_age.max(carried); } self } } pub fn plan( status: StatusCode, headers: &HeaderMap, settings: &Settings, response_delay: Duration, ) -> Option { if ["age", "date", "expires", "content-length"] .iter() .any(|name| headers.get_all(*name).iter().count() > 1) { return None; } if status != StatusCode::OK || headers.contains_key("set-cookie") || headers.contains_key("content-range") || headers.contains_key("trailer") { return None; } let bytes: usize = headers .iter() .map(|(k, v)| k.as_str().len() + v.as_bytes().len() + 32) .sum(); if bytes > MAX_HEADER_BYTES { return None; } if headers .get("content-type") .and_then(|v| v.to_str().ok()) .is_some_and(|v| { let v = v.trim_start().to_ascii_lowercase(); v.starts_with("text/event-stream") || v.starts_with("application/grpc") }) { return None; } let vary = vary_names(headers)?; let control = directives(headers)?; if ["no-store", "private", "must-understand"] .iter() .any(|name| control.contains_key(*name)) { return None; } // Surrogate-Control: no-store is a shared-cache veto in compatibility // mode, regardless of public/fallback TTL or any other directive. 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. let override_public_no_cache = settings.varnish_compatible && control.contains_key("no-cache") && control.contains_key("public") && settings.default_ttl_seconds > 0; let requires_revalidation = control.contains_key("no-cache") && !override_public_no_cache; let must_revalidate = control.contains_key("must-revalidate"); let proxy_revalidate = control.contains_key("proxy-revalidate"); let parse_window = |name: &str| -> Option { match control.get(name) { None => Some(0), Some(Some(value)) => value.parse::().ok().map(|v| v.min(604_800)), Some(None) => None, } }; 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") { return None; } let now = SystemTime::now(); let date = match headers.get("date") { 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::().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::().ok()?, None => 0, }; let apparent_age = now.duration_since(date).unwrap_or_default(); let corrected_age_value = Duration::from_secs(age).saturating_add(response_delay); let initial_age = apparent_age.max(corrected_age_value); let ttl = match declared_ttl { Some(value) => value.min( initial_age .as_secs() .saturating_add(settings.max_ttl_seconds), ), 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. if settings.varnish_compatible && !control.contains_key("stale-while-revalidate") && !control.contains_key("no-cache") && initial_age.as_secs() < ttl { stale_while_revalidate = settings.varnish_grace_seconds; } if !requires_revalidation && initial_age.as_secs() >= retention_deadline( ttl, stale_while_revalidate, stale_if_error, has_validator(headers), ) { return None; } let expected_length = match headers.get("content-length") { Some(v) => Some(v.to_str().ok()?.parse::().ok()?), None => None, }; if expected_length.is_some_and(|n| n > settings.max_object_bytes) { return None; } Some(Plan { received: Instant::now(), initial_age, ttl, stale_while_revalidate, stale_if_error, must_revalidate, proxy_revalidate, expected_length, requires_revalidation, vary, }) }