use crate::policy::CachePolicy;
use std::time::SystemTime;
use trillium_caching_headers::CachingHeadersExt;
use trillium_http::{Headers, KnownHeaderName, Method, Status};
impl CachePolicy {
pub(crate) fn before_request(
&self,
request_headers: &Headers,
now: SystemTime,
) -> BeforeRequest {
let matches = self.vary_matches(request_headers);
if matches && self.satisfies_without_revalidation(request_headers, now) {
if self.inbound_conditional_matches(request_headers) {
return BeforeRequest::NotModified(self.cached_response_304(now));
}
BeforeRequest::Fresh(self.cached_response(now))
} else {
BeforeRequest::Stale {
request_headers: self.revalidation_request_headers(request_headers),
matches,
}
}
}
fn inbound_conditional_matches(&self, request_headers: &Headers) -> bool {
if let Some(inm) = request_headers.get_str(KnownHeaderName::IfNoneMatch) {
return inm_matches(inm, self.response_headers.get_str(KnownHeaderName::Etag));
}
if let Some(ims) = request_headers.get_str(KnownHeaderName::IfModifiedSince) {
return ims_matches(
ims,
self.response_headers.get_str(KnownHeaderName::LastModified),
);
}
false
}
fn cached_response_304(&self, now: SystemTime) -> CachedResponse {
const HEADERS_TO_KEEP: &[KnownHeaderName] = &[
KnownHeaderName::Date,
KnownHeaderName::Etag,
KnownHeaderName::LastModified,
KnownHeaderName::Vary,
KnownHeaderName::CacheControl,
KnownHeaderName::ContentLocation,
KnownHeaderName::Expires,
];
let mut headers = Headers::new();
for name in HEADERS_TO_KEEP {
if let Some(value) = self.response_headers.get_values(*name) {
headers.insert(*name, value.clone());
}
}
let age = self.age(now);
headers.insert(KnownHeaderName::Age, age.as_secs().to_string());
CachedResponse {
status: Status::NotModified,
headers,
}
}
fn vary_matches(&self, request_headers: &Headers) -> bool {
for (name, stored_value) in &self.vary_snapshot {
if name == "*" {
return false;
}
let new_value = request_headers.get_str(name.as_str());
if new_value != stored_value.as_deref() {
return false;
}
}
true
}
fn satisfies_without_revalidation(&self, request_headers: &Headers, now: SystemTime) -> bool {
let req_cc = request_headers.cache_control();
if req_cc.as_ref().is_some_and(|cc| cc.is_no_cache())
|| request_headers
.get_str(KnownHeaderName::Pragma)
.is_some_and(|p| p.contains("no-cache"))
{
return false;
}
if let Some(req_max_age) = req_cc.as_ref().and_then(|cc| cc.max_age())
&& self.age(now) > req_max_age
{
return false;
}
if let Some(min_fresh) = req_cc.as_ref().and_then(|cc| cc.min_fresh())
&& self.time_to_live(now) < min_fresh
{
return false;
}
if self.is_stale(now) {
let max_stale = req_cc.as_ref().and_then(|cc| cc.max_stale());
let must_revalidate = self
.response_cache_control
.as_ref()
.is_some_and(|cc| cc.must_revalidate());
let allows_stale = !must_revalidate
&& match max_stale {
None => false,
Some(None) => true,
Some(Some(allowed)) => allowed > self.age(now) - self.max_age(),
};
if !allows_stale {
return false;
}
}
true
}
fn revalidation_request_headers(&self, request_headers: &Headers) -> Headers {
let mut headers = copy_without_hop_by_hop_headers(request_headers);
headers.remove(KnownHeaderName::IfRange);
if let Some(etag) = self.response_headers.get_str(KnownHeaderName::Etag) {
let combined = match headers.get_str(KnownHeaderName::IfNoneMatch) {
Some(existing) => format!("{existing}, {etag}"),
None => etag.to_string(),
};
headers.insert(KnownHeaderName::IfNoneMatch, combined);
}
let forbids_weak_validators = self.request_method != Method::Get
|| headers.has_header(KnownHeaderName::AcceptRanges)
|| headers.has_header(KnownHeaderName::IfMatch)
|| headers.has_header(KnownHeaderName::IfUnmodifiedSince);
if forbids_weak_validators {
headers.remove(KnownHeaderName::IfModifiedSince);
if let Some(inm) = headers.get_str(KnownHeaderName::IfNoneMatch) {
let strong: String = inm
.split(',')
.map(str::trim)
.filter(|t| !t.starts_with("W/"))
.collect::<Vec<_>>()
.join(", ");
if strong.is_empty() {
headers.remove(KnownHeaderName::IfNoneMatch);
} else {
headers.insert(KnownHeaderName::IfNoneMatch, strong);
}
}
} else if !headers.has_header(KnownHeaderName::IfModifiedSince) {
if let Some(lm) = self.response_headers.get_str(KnownHeaderName::LastModified) {
headers.insert(KnownHeaderName::IfModifiedSince, lm.to_string());
}
}
headers
}
pub(crate) fn cached_response(&self, now: SystemTime) -> CachedResponse {
let mut headers = copy_without_hop_by_hop_headers(&self.response_headers);
let age = self.age(now);
headers.insert(KnownHeaderName::Age, age.as_secs().to_string());
CachedResponse {
status: self.response_status,
headers,
}
}
pub(crate) fn after_response(
&self,
request_headers: &Headers,
new_status: Status,
new_response_headers: &Headers,
response_time: SystemTime,
) -> AfterResponse {
let entity_matches = new_status == Status::NotModified;
let (final_status, final_response_headers) = if entity_matches {
(
self.response_status,
merge_revalidation_headers(&self.response_headers, new_response_headers),
)
} else {
(new_status, new_response_headers.clone())
};
if entity_matches {
let new_policy = CachePolicy::new(
self.request_method,
request_headers,
final_status,
final_response_headers,
response_time,
self.options,
);
let new_response = new_policy.cached_response(response_time);
AfterResponse::NotModified(new_policy, new_response)
} else {
AfterResponse::Modified
}
}
}
fn merge_revalidation_headers(stored: &Headers, new: &Headers) -> Headers {
let mut out = stored.clone();
for (name, values) in new.iter() {
if is_excluded_from_revalidation_update(&name) {
continue;
}
out.insert(name.into_owned(), values.clone());
}
out
}
fn is_excluded_from_revalidation_update(name: &trillium_http::HeaderName<'_>) -> bool {
name == KnownHeaderName::ContentLength
|| name == KnownHeaderName::ContentEncoding
|| name == KnownHeaderName::TransferEncoding
|| name == KnownHeaderName::ContentRange
}
fn inm_matches(if_none_match: &str, cached_etag: Option<&str>) -> bool {
let inm = if_none_match.trim();
if inm == "*" {
return true;
}
let Some(cached_opaque) = cached_etag.and_then(etag_opaque) else {
return false;
};
iter_etag_opaques(inm).any(|tag| tag == cached_opaque)
}
fn ims_matches(if_modified_since: &str, cached_last_modified: Option<&str>) -> bool {
let Ok(ims) = httpdate::parse_http_date(if_modified_since) else {
return false;
};
let Some(lm_str) = cached_last_modified else {
return false;
};
let Ok(lm) = httpdate::parse_http_date(lm_str) else {
return false;
};
lm <= ims
}
fn etag_opaque(etag: &str) -> Option<&str> {
let etag = etag.trim();
let etag = etag.strip_prefix("W/").unwrap_or(etag);
let inner = etag.strip_prefix('"')?;
inner.strip_suffix('"')
}
fn iter_etag_opaques(s: &str) -> impl Iterator<Item = &str> + '_ {
let mut remaining = s;
std::iter::from_fn(move || {
loop {
remaining = remaining.trim_start();
if let Some(rest) = remaining.strip_prefix(',') {
remaining = rest;
continue;
}
if let Some(rest) = remaining.strip_prefix("W/") {
remaining = rest;
continue;
}
break;
}
let inner = remaining.strip_prefix('"')?;
let close = inner.find('"')?;
let tag = &inner[..close];
remaining = &inner[close + 1..];
Some(tag)
})
}
fn copy_without_hop_by_hop_headers(headers: &Headers) -> Headers {
let mut out = headers.clone();
out.remove_all([
KnownHeaderName::Connection,
KnownHeaderName::KeepAlive,
KnownHeaderName::ProxyAuthenticate,
KnownHeaderName::ProxyAuthorization,
KnownHeaderName::Te,
KnownHeaderName::Trailer,
KnownHeaderName::TransferEncoding,
KnownHeaderName::Upgrade,
]);
if let Some(connection) = headers.get_str(KnownHeaderName::Connection) {
for name in connection
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
{
out.remove(name);
}
}
out
}
#[derive(Debug, Clone)]
pub(crate) enum BeforeRequest {
Fresh(CachedResponse),
NotModified(CachedResponse),
Stale {
request_headers: Headers,
matches: bool,
},
}
#[derive(Debug, Clone)]
pub(crate) struct CachedResponse {
pub(crate) status: Status,
pub(crate) headers: Headers,
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum AfterResponse {
NotModified(CachePolicy, CachedResponse),
Modified,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
use std::time::Duration;
use trillium_client::ConnExt;
use trillium_http::KnownHeaderName::*;
#[test]
fn before_request_fresh_returns_cached_response() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[
(Date, "Thu, 01 Jan 1970 00:00:00 GMT"),
(CacheControl, "max-age=600"),
(Etag, r#""abc""#),
],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(Method::Get, &[]);
match before_request(&policy, &new, at(t0(), 100)) {
BeforeRequest::Fresh(cached) => {
assert_eq!(cached.status, Status::Ok);
assert_eq!(cached.headers.get_str(Age), Some("100"));
assert_eq!(
cached.headers.get_str(Date),
Some("Thu, 01 Jan 1970 00:00:00 GMT"),
"origin Date is preserved on cache hit"
);
assert_eq!(cached.headers.get_str(Etag), Some(r#""abc""#));
}
other => panic!("expected Fresh, got {other:?}"),
}
}
#[test]
fn before_request_stale_uses_etag_for_revalidation() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=10"), (Etag, r#""abc""#)],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(Method::Get, &[]);
match before_request(&policy, &new, at(t0(), 1000)) {
BeforeRequest::Stale {
request_headers,
matches,
} => {
assert!(matches);
assert_eq!(request_headers.get_str(IfNoneMatch), Some(r#""abc""#));
}
other => panic!("expected Stale, got {other:?}"),
}
}
#[test]
fn before_request_stale_uses_last_modified_for_revalidation() {
let lm = httpdate::fmt_http_date(t0() - Duration::from_secs(86400));
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=10"), (LastModified, &lm)],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(Method::Get, &[]);
match before_request(&policy, &new, at(t0(), 1000)) {
BeforeRequest::Stale {
request_headers, ..
} => {
assert_eq!(request_headers.get_str(IfModifiedSince), Some(lm.as_str()));
}
other => panic!("expected Stale, got {other:?}"),
}
}
#[test]
fn vary_mismatch_returns_stale_not_matching() {
let stored = exchange(
Method::Get,
&[(AcceptEncoding, "gzip")],
Status::Ok,
&[(CacheControl, "max-age=600"), (Vary, "Accept-Encoding")],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(Method::Get, &[(AcceptEncoding, "br")]);
match before_request(&policy, &new, t0()) {
BeforeRequest::Stale { matches, .. } => assert!(!matches),
other => panic!("expected Stale, got {other:?}"),
}
}
#[test]
fn vary_match_returns_fresh() {
let stored = exchange(
Method::Get,
&[(AcceptEncoding, "gzip")],
Status::Ok,
&[(CacheControl, "max-age=600"), (Vary, "Accept-Encoding")],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(Method::Get, &[(AcceptEncoding, "gzip")]);
assert!(matches!(
before_request(&policy, &new, t0()),
BeforeRequest::Fresh(_)
));
}
#[test]
fn request_no_cache_forces_stale() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=600")],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(Method::Get, &[(CacheControl, "no-cache")]);
assert!(matches!(
before_request(&policy, &new, t0()),
BeforeRequest::Stale { .. }
));
}
#[test]
fn request_pragma_no_cache_forces_stale() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=600")],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(Method::Get, &[(Pragma, "no-cache")]);
assert!(matches!(
before_request(&policy, &new, t0()),
BeforeRequest::Stale { .. }
));
}
#[test]
fn request_max_age_limits_freshness() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=600")],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(Method::Get, &[(CacheControl, "max-age=50")]);
assert!(matches!(
before_request(&policy, &new, at(t0(), 100)),
BeforeRequest::Stale { .. }
));
}
#[test]
fn request_min_fresh_demands_headroom() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=600")],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(Method::Get, &[(CacheControl, "min-fresh=600")]);
assert!(matches!(
before_request(&policy, &new, at(t0(), 100)),
BeforeRequest::Stale { .. }
));
}
#[test]
fn request_max_stale_allows_stale_response() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=100")],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(Method::Get, &[(CacheControl, "max-stale=200")]);
assert!(matches!(
before_request(&policy, &new, at(t0(), 150)),
BeforeRequest::Fresh(_)
));
}
#[test]
fn must_revalidate_ignores_max_stale() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=100, must-revalidate")],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(Method::Get, &[(CacheControl, "max-stale=200")]);
assert!(matches!(
before_request(&policy, &new, at(t0(), 150)),
BeforeRequest::Stale { .. }
));
}
#[test]
fn cached_response_strips_hop_by_hop_headers() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[
(CacheControl, "max-age=600"),
(Connection, "close"),
(TransferEncoding, "chunked"),
],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(Method::Get, &[]);
match before_request(&policy, &new, t0()) {
BeforeRequest::Fresh(cached) => {
assert!(!cached.headers.has_header(Connection));
assert!(!cached.headers.has_header(TransferEncoding));
}
other => panic!("expected Fresh, got {other:?}"),
}
}
#[test]
fn connection_header_value_strips_named_headers() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=600"), (Connection, "X-Custom-Hop")],
);
let stored = {
let mut s = stored;
s.response_headers_mut()
.insert("x-custom-hop", "value".to_string());
s
};
let policy = policy_from(&stored, t0(), private_cache());
let new = request(Method::Get, &[]);
match before_request(&policy, &new, t0()) {
BeforeRequest::Fresh(cached) => {
assert!(!cached.headers.has_header("x-custom-hop"));
}
other => panic!("expected Fresh, got {other:?}"),
}
}
#[test]
fn after_response_304_strong_etag_match() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=10"), (Etag, r#""v1""#)],
);
let policy = policy_from(&stored, t0(), private_cache());
let revalidation = exchange(
Method::Get,
&[],
Status::NotModified,
&[(Etag, r#""v1""#), (CacheControl, "max-age=600")],
);
match after_response(&policy, &revalidation, at(t0(), 100)) {
AfterResponse::NotModified(new_policy, cached) => {
assert_eq!(cached.status, Status::Ok);
assert_eq!(
new_policy.response_headers.get_str(CacheControl),
Some("max-age=600")
);
assert_eq!(new_policy.response_headers.get_str(Etag), Some(r#""v1""#));
}
other => panic!("expected NotModified, got {other:?}"),
}
}
#[test]
fn after_response_304_with_different_etag_updates_stored_validator() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=10"), (Etag, r#""v1""#)],
);
let policy = policy_from(&stored, t0(), private_cache());
let revalidation = exchange(Method::Get, &[], Status::NotModified, &[(Etag, r#""v2""#)]);
match after_response(&policy, &revalidation, at(t0(), 100)) {
AfterResponse::NotModified(new_policy, _) => {
assert_eq!(
new_policy.response_headers.get_str(Etag),
Some(r#""v2""#),
"304's new ETag replaces the stored one"
);
}
other => panic!("expected NotModified, got {other:?}"),
}
}
#[test]
fn after_response_304_weak_etag_match() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=10"), (Etag, r#"W/"v1""#)],
);
let policy = policy_from(&stored, t0(), private_cache());
let revalidation = exchange(
Method::Get,
&[],
Status::NotModified,
&[(Etag, r#"W/"v1""#)],
);
assert!(matches!(
after_response(&policy, &revalidation, at(t0(), 100)),
AfterResponse::NotModified(..)
));
}
#[test]
fn after_response_304_last_modified_match() {
let lm = httpdate::fmt_http_date(t0() - Duration::from_secs(86400));
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=10"), (LastModified, &lm)],
);
let policy = policy_from(&stored, t0(), private_cache());
let revalidation = exchange(
Method::Get,
&[],
Status::NotModified,
&[(LastModified, &lm)],
);
assert!(matches!(
after_response(&policy, &revalidation, at(t0(), 100)),
AfterResponse::NotModified(..)
));
}
#[test]
fn after_response_304_without_validators_trusts_stored_entry() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=10"), (Etag, r#""abcdef""#)],
);
let policy = policy_from(&stored, t0(), private_cache());
let revalidation = exchange(Method::Get, &[], Status::NotModified, &[]);
assert!(matches!(
after_response(&policy, &revalidation, at(t0(), 100)),
AfterResponse::NotModified(..)
));
}
#[test]
fn after_response_200_is_modified() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=10"), (Etag, r#""v1""#)],
);
let policy = policy_from(&stored, t0(), private_cache());
let fresh = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=600"), (Etag, r#""v2""#)],
);
assert!(matches!(
after_response(&policy, &fresh, at(t0(), 100)),
AfterResponse::Modified
));
}
#[test]
fn after_response_merge_preserves_body_headers_from_stored() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[
(CacheControl, "max-age=10"),
(Etag, r#""v1""#),
(ContentLength, "1234"),
(ContentEncoding, "gzip"),
],
);
let policy = policy_from(&stored, t0(), private_cache());
let revalidation = exchange(
Method::Get,
&[],
Status::NotModified,
&[
(Etag, r#""v1""#),
(ContentLength, "0"),
(ContentEncoding, "identity"),
(CacheControl, "max-age=600"),
],
);
match after_response(&policy, &revalidation, at(t0(), 100)) {
AfterResponse::NotModified(new_policy, _) => {
assert_eq!(
new_policy.response_headers.get_str(ContentLength),
Some("1234")
);
assert_eq!(
new_policy.response_headers.get_str(ContentEncoding),
Some("gzip")
);
assert_eq!(
new_policy.response_headers.get_str(CacheControl),
Some("max-age=600")
);
}
other => panic!("expected NotModified, got {other:?}"),
}
}
#[test]
fn after_response_merge_includes_new_304_headers() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=10"), (Etag, r#""v1""#)],
);
let policy = policy_from(&stored, t0(), private_cache());
let revalidation = exchange(
Method::Get,
&[],
Status::NotModified,
&[(Etag, r#""v1""#), (Vary, "Accept-Encoding")],
);
match after_response(&policy, &revalidation, at(t0(), 100)) {
AfterResponse::NotModified(new_policy, _) => {
assert_eq!(
new_policy.response_headers.get_str(Vary),
Some("Accept-Encoding")
);
}
other => panic!("expected NotModified, got {other:?}"),
}
}
#[test]
fn after_response_no_validators_treated_as_match() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "max-age=10")],
);
let policy = policy_from(&stored, t0(), private_cache());
let revalidation = exchange(
Method::Get,
&[],
Status::NotModified,
&[(CacheControl, "max-age=600")],
);
assert!(matches!(
after_response(&policy, &revalidation, at(t0(), 100)),
AfterResponse::NotModified(..)
));
}
#[test]
fn etag_opaque_strips_quotes_and_weak_prefix() {
assert_eq!(etag_opaque(r#""abc""#), Some("abc"));
assert_eq!(etag_opaque(r#"W/"abc""#), Some("abc"));
assert_eq!(etag_opaque(" \"abc\" "), Some("abc"));
assert_eq!(etag_opaque("abc"), None); assert_eq!(etag_opaque(""), None);
}
#[test]
fn iter_etag_opaques_handles_multi_value_lists() {
let v: Vec<&str> = iter_etag_opaques(r#""a", W/"b", "c""#).collect();
assert_eq!(v, vec!["a", "b", "c"]);
let v: Vec<&str> = iter_etag_opaques(r#""only""#).collect();
assert_eq!(v, vec!["only"]);
let v: Vec<&str> = iter_etag_opaques("").collect();
assert!(v.is_empty());
}
#[test]
fn inm_matches_strong_and_weak_forms_via_weak_comparison() {
assert!(inm_matches(r#""abc""#, Some(r#""abc""#)));
assert!(inm_matches(r#"W/"abc""#, Some(r#""abc""#)));
assert!(inm_matches(r#""abc""#, Some(r#"W/"abc""#)));
assert!(inm_matches(r#"W/"abc""#, Some(r#"W/"abc""#)));
assert!(inm_matches(r#""x", "abc", "y""#, Some(r#""abc""#)));
assert!(!inm_matches(r#""def""#, Some(r#""abc""#)));
assert!(!inm_matches(r#""abc""#, None));
assert!(inm_matches("*", Some(r#""abc""#)));
assert!(inm_matches("*", None)); }
#[test]
fn ims_matches_when_cached_lm_is_no_later_than_request_ims() {
let lm_str = "Thu, 01 Jan 1970 00:00:00 GMT";
let ims_str = "Thu, 01 Jan 1970 00:00:01 GMT";
assert!(ims_matches(ims_str, Some(lm_str)));
assert!(!ims_matches(lm_str, Some(ims_str)));
assert!(ims_matches(lm_str, Some(lm_str)));
assert!(!ims_matches(ims_str, None));
assert!(!ims_matches("not-a-date", Some(lm_str)));
assert!(!ims_matches(ims_str, Some("not-a-date")));
}
#[test]
fn before_request_returns_not_modified_when_inm_matches() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[
(Date, "Thu, 01 Jan 1970 00:00:00 GMT"),
(CacheControl, "max-age=600"),
(Etag, r#""abcdef""#),
(ContentLength, "1234"),
(ContentType, "application/json"),
],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(Method::Get, &[(IfNoneMatch, r#""abcdef""#)]);
match before_request(&policy, &new, at(t0(), 100)) {
BeforeRequest::NotModified(cached) => {
assert_eq!(cached.status, Status::NotModified);
assert_eq!(cached.headers.get_str(Etag), Some(r#""abcdef""#));
assert_eq!(cached.headers.get_str(Age), Some("100"));
assert_eq!(cached.headers.get_str(CacheControl), Some("max-age=600"));
assert!(!cached.headers.has_header(ContentLength));
assert!(!cached.headers.has_header(ContentType));
}
other => panic!("expected NotModified, got {other:?}"),
}
}
#[test]
fn before_request_inm_takes_precedence_over_ims() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[
(CacheControl, "max-age=600"),
(Etag, r#""abcdef""#),
(LastModified, "Thu, 01 Jan 1970 00:00:00 GMT"),
],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(
Method::Get,
&[
(IfNoneMatch, r#""different""#),
(IfModifiedSince, "Thu, 01 Jan 1970 00:00:01 GMT"),
],
);
assert!(matches!(
before_request(&policy, &new, at(t0(), 100)),
BeforeRequest::Fresh(_)
));
}
#[test]
fn before_request_falls_back_to_ims_when_inm_absent() {
let stored = exchange(
Method::Get,
&[],
Status::Ok,
&[
(CacheControl, "max-age=600"),
(LastModified, "Thu, 01 Jan 1970 00:00:00 GMT"),
],
);
let policy = policy_from(&stored, t0(), private_cache());
let new = request(
Method::Get,
&[(IfModifiedSince, "Thu, 01 Jan 1970 00:00:01 GMT")],
);
assert!(matches!(
before_request(&policy, &new, at(t0(), 100)),
BeforeRequest::NotModified(_)
));
}
}