use crate::{
CacheOptions,
policy::{CachePolicy, effective_response_cache_control},
};
use trillium_caching_headers::{CacheControlHeader, CachingHeadersExt};
use trillium_http::{Headers, KnownHeaderName, Method, Status};
const STATUS_CODE_CACHEABLE_BY_DEFAULT: &[u16] =
&[200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501];
fn is_cacheable_by_default(status: Status) -> bool {
STATUS_CODE_CACHEABLE_BY_DEFAULT.contains(&(status as u16))
}
pub(crate) fn response_has_explicit_expiration(
response_cc: &Option<CacheControlHeader>,
response_headers: &Headers,
options: &CacheOptions,
targeted_cc_in_effect: bool,
) -> bool {
if options.shared
&& response_cc
.as_ref()
.is_some_and(|cc| cc.s_maxage().is_some())
{
return true;
}
if response_cc
.as_ref()
.is_some_and(|cc| cc.max_age().is_some())
{
return true;
}
!targeted_cc_in_effect && response_headers.has_header(KnownHeaderName::Expires)
}
impl CachePolicy {
pub(crate) fn is_storable(
method: Method,
request_headers: &Headers,
status: Status,
response_headers: &Headers,
options: &CacheOptions,
) -> bool {
let request_cc = request_headers.cache_control();
let (response_cc, targeted_cc_in_effect) =
effective_response_cache_control(response_headers, options);
if request_cc.as_ref().is_some_and(|cc| cc.is_no_store()) {
return false;
}
let method_ok = matches!(method, Method::Get | Method::Head)
|| (method == Method::Post
&& response_has_explicit_expiration(
&response_cc,
response_headers,
options,
targeted_cc_in_effect,
));
if !method_ok {
return false;
}
if response_cc.as_ref().is_some_and(|cc| cc.is_no_store()) {
return false;
}
if options.shared && response_cc.as_ref().is_some_and(|cc| cc.is_private()) {
return false;
}
if options.shared
&& request_headers.has_header(KnownHeaderName::Authorization)
&& !response_cc
.as_ref()
.is_some_and(|cc| cc.must_revalidate() || cc.is_public() || cc.s_maxage().is_some())
{
return false;
}
(!targeted_cc_in_effect && response_headers.has_header(KnownHeaderName::Expires))
|| response_cc.as_ref().is_some_and(|cc| {
cc.max_age().is_some()
|| cc.is_public()
|| (options.shared && cc.s_maxage().is_some())
})
|| is_cacheable_by_default(status)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
use trillium_http::KnownHeaderName::*;
#[test]
fn no_store_request_blocks_storage() {
let conn = exchange(Method::Get, &[(CacheControl, "no-store")], Status::Ok, &[]);
assert!(!is_storable(&conn, &private_cache()));
}
#[test]
fn no_store_response_blocks_storage() {
let conn = exchange(Method::Get, &[], Status::Ok, &[(CacheControl, "no-store")]);
assert!(!is_storable(&conn, &private_cache()));
}
#[test]
fn cacheable_by_default_status_is_storable() {
let conn = exchange(Method::Get, &[], Status::Ok, &[]);
assert!(is_storable(&conn, &private_cache()));
}
#[test]
fn non_default_status_not_storable_without_freshness() {
let conn = exchange(Method::Get, &[], Status::InternalServerError, &[]);
assert!(!is_storable(&conn, &private_cache()));
}
#[test]
fn non_default_status_storable_with_explicit_freshness() {
let conn = exchange(
Method::Get,
&[],
Status::InternalServerError,
&[(CacheControl, "max-age=600")],
);
assert!(is_storable(&conn, &private_cache()));
let conn = exchange(
Method::Get,
&[],
Status::BadRequest,
&[(CacheControl, "max-age=600")],
);
assert!(is_storable(&conn, &private_cache()));
}
#[test]
fn put_not_storable() {
let conn = exchange(Method::Put, &[], Status::Ok, &[]);
assert!(!is_storable(&conn, &private_cache()));
}
#[test]
fn post_with_max_age_is_storable() {
let conn = exchange(
Method::Post,
&[],
Status::Ok,
&[(CacheControl, "max-age=600")],
);
assert!(is_storable(&conn, &private_cache()));
}
#[test]
fn post_without_expiration_not_storable() {
let conn = exchange(Method::Post, &[], Status::Ok, &[]);
assert!(!is_storable(&conn, &private_cache()));
}
#[test]
fn shared_cache_refuses_private_response() {
let conn = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "private, max-age=600")],
);
assert!(!is_storable(&conn, &shared_cache()));
}
#[test]
fn private_cache_accepts_private_response() {
let conn = exchange(
Method::Get,
&[],
Status::Ok,
&[(CacheControl, "private, max-age=600")],
);
assert!(is_storable(&conn, &private_cache()));
}
#[test]
fn shared_cache_refuses_authorization_by_default() {
let conn = exchange(
Method::Get,
&[(Authorization, "Bearer x")],
Status::Ok,
&[(CacheControl, "max-age=600")],
);
assert!(!is_storable(&conn, &shared_cache()));
}
#[test]
fn shared_cache_allows_authorization_with_public() {
let conn = exchange(
Method::Get,
&[(Authorization, "Bearer x")],
Status::Ok,
&[(CacheControl, "public, max-age=600")],
);
assert!(is_storable(&conn, &shared_cache()));
}
#[test]
fn shared_cache_allows_authorization_with_s_maxage() {
let conn = exchange(
Method::Get,
&[(Authorization, "Bearer x")],
Status::Ok,
&[(CacheControl, "s-maxage=600")],
);
assert!(is_storable(&conn, &shared_cache()));
}
#[test]
fn shared_cache_allows_authorization_with_must_revalidate() {
let conn = exchange(
Method::Get,
&[(Authorization, "Bearer x")],
Status::Ok,
&[(CacheControl, "must-revalidate, max-age=600")],
);
assert!(is_storable(&conn, &shared_cache()));
}
#[test]
fn private_cache_allows_authorization() {
let conn = exchange(
Method::Get,
&[(Authorization, "Bearer x")],
Status::Ok,
&[(CacheControl, "max-age=600")],
);
assert!(is_storable(&conn, &private_cache()));
}
#[test]
fn pragma_no_cache_does_not_block_storage() {
let conn = exchange(Method::Get, &[], Status::Ok, &[(Pragma, "no-cache")]);
assert!(is_storable(&conn, &private_cache()));
}
#[test]
fn shared_cache_stores_when_cdn_cc_overrides_cc_no_store() {
let conn = exchange(
Method::Get,
&[],
Status::Ok,
&[
(CacheControl, "no-store"),
(CdnCacheControl, "max-age=10000"),
],
);
assert!(is_storable(&conn, &shared_cache()));
}
#[test]
fn shared_cache_refuses_when_cdn_cc_no_store_overrides_cc_max_age() {
let conn = exchange(
Method::Get,
&[],
Status::Ok,
&[
(CacheControl, "max-age=10000"),
(CdnCacheControl, "no-store"),
],
);
assert!(!is_storable(&conn, &shared_cache()));
}
#[test]
fn private_cache_ignores_cdn_cache_control() {
let conn = exchange(
Method::Get,
&[],
Status::Ok,
&[
(CacheControl, "max-age=10000"),
(CdnCacheControl, "no-store"),
],
);
assert!(is_storable(&conn, &private_cache()));
}
}