use crate::{
error::Error,
headers::{CACHE_CONTROL, ETag, FromHeaders, Header, HeaderMap, HeaderName, HeaderValue},
};
use std::{fmt, time::SystemTime};
#[cfg(feature = "static-files")]
use std::fs::Metadata;
#[cfg(feature = "middleware")]
use {
crate::{
App, HttpResponse, HttpResult,
routing::{Route, RouteGroup},
},
std::future::Future,
};
#[cfg(feature = "static-files")]
const DEFAULT_MAX_AGE: u32 = 60 * 60 * 24;
pub const NO_STORE: &str = "no-store";
pub const NO_CACHE: &str = "no-cache";
pub const MAX_AGE: &str = "max-age";
pub const S_MAX_AGE: &str = "s-maxage";
pub const MUST_REVALIDATE: &str = "must-revalidate";
pub const PROXY_REVALIDATE: &str = "proxy-revalidate";
pub const PUBLIC: &str = "public";
pub const PRIVATE: &str = "private";
pub const IMMUTABLE: &str = "immutable";
pub const STALE_IF_ERROR: &str = "stale-if-error";
pub const STALE_WHILE_REVALIDATE: &str = "stale-while-revalidate";
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CacheControl {
no_cache: bool,
no_store: bool,
max_age: Option<u32>,
s_max_age: Option<u32>,
must_revalidate: bool,
proxy_revalidate: bool,
stale_while_revalidate: Option<u32>,
stale_if_error: Option<u32>,
public: bool,
private: bool,
immutable: bool,
}
impl FromHeaders for CacheControl {
const NAME: HeaderName = CACHE_CONTROL;
#[inline]
fn from_headers(headers: &HeaderMap) -> Option<&HeaderValue> {
headers.get(Self::NAME)
}
}
impl fmt::Display for CacheControl {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut directives = Vec::new();
if self.no_cache {
directives.push(NO_CACHE.to_string());
}
if self.no_store {
directives.push(NO_STORE.to_string());
}
if let Some(max_age) = self.max_age {
directives.push(format!("{MAX_AGE}={max_age}"));
}
if let Some(s_max_age) = self.s_max_age {
directives.push(format!("{S_MAX_AGE}={s_max_age}"));
}
if self.must_revalidate {
directives.push(MUST_REVALIDATE.to_string());
}
if self.proxy_revalidate {
directives.push(PROXY_REVALIDATE.to_string());
}
if self.public {
directives.push(PUBLIC.to_string());
}
if self.private {
directives.push(PRIVATE.to_string());
}
if self.immutable {
directives.push(IMMUTABLE.to_string());
}
if let Some(stale_while_revalidate) = self.stale_while_revalidate {
directives.push(format!("{STALE_WHILE_REVALIDATE}={stale_while_revalidate}"));
}
if let Some(stale_if_error) = self.stale_if_error {
directives.push(format!("{STALE_IF_ERROR}={stale_if_error}"));
}
f.write_str(directives.join(", ").as_str())
}
}
impl From<CacheControl> for String {
#[inline]
fn from(cc: CacheControl) -> Self {
cc.to_string()
}
}
impl TryFrom<CacheControl> for HeaderValue {
type Error = Error;
#[inline]
fn try_from(value: CacheControl) -> Result<Self, Self::Error> {
HeaderValue::from_str(value.to_string().as_str()).map_err(Into::into)
}
}
impl TryFrom<CacheControl> for Header<CacheControl> {
type Error = Error;
#[inline]
fn try_from(value: CacheControl) -> Result<Self, Self::Error> {
Ok(Self::new(value.try_into()?))
}
}
impl CacheControl {
#[inline(always)]
pub const fn from_static(value: &'static str) -> Header<Self> {
Header::<Self>::from_static(value)
}
#[inline]
pub fn from_bytes(bytes: &[u8]) -> Result<Header<Self>, Error> {
Header::<Self>::from_bytes(bytes)
}
#[inline]
pub fn new(value: HeaderValue) -> Header<Self> {
Header::<Self>::new(value)
}
#[inline]
pub fn from_ref(value: &HeaderValue) -> Header<Self> {
Header::<Self>::from_ref(value)
}
pub fn with_no_cache(mut self) -> Self {
self.no_cache = true;
self.immutable = false;
self
}
pub fn with_no_store(mut self) -> Self {
self.no_store = true;
self.max_age = None;
self.s_max_age = None;
self.stale_if_error = None;
self.stale_while_revalidate = None;
self
}
pub fn with_max_age(mut self, max_age: u32) -> Self {
self.max_age = Some(max_age);
self.no_store = false;
self
}
pub fn with_s_max_age(mut self, s_max_age: u32) -> Self {
self.s_max_age = Some(s_max_age);
self.no_store = false;
self
}
pub fn with_must_revalidate(mut self) -> Self {
self.must_revalidate = true;
self.immutable = false;
self
}
pub fn with_proxy_revalidate(mut self) -> Self {
self.proxy_revalidate = true;
self.immutable = false;
self
}
pub fn with_public(mut self) -> Self {
self.public = true;
self.private = false;
self
}
pub fn with_private(mut self) -> Self {
self.private = true;
self.public = false;
self
}
pub fn with_immutable(mut self) -> Self {
self.immutable = true;
self.no_cache = false;
self.proxy_revalidate = false;
self.must_revalidate = false;
self
}
pub fn with_stale_while_revalidate(mut self, age: u32) -> Self {
self.stale_while_revalidate = Some(age);
self.no_store = false;
self
}
pub fn with_stale_if_error(mut self, age: u32) -> Self {
self.stale_if_error = Some(age);
self.no_store = false;
self
}
}
#[derive(Debug)]
pub struct ResponseCaching {
pub(crate) etag: ETag,
pub(crate) last_modified: SystemTime,
pub(crate) cache_control: CacheControl,
}
#[cfg(feature = "static-files")]
impl TryFrom<&Metadata> for ResponseCaching {
type Error = Error;
#[inline]
fn try_from(meta: &Metadata) -> Result<Self, Self::Error> {
let last_modified = meta.modified()?;
let etag: ETag = meta.try_into()?;
let cache_control = CacheControl {
public: true,
immutable: true,
max_age: Some(DEFAULT_MAX_AGE),
..Default::default()
};
let this = Self {
cache_control,
etag,
last_modified,
};
Ok(this)
}
}
impl ResponseCaching {
#[inline]
pub fn etag(&self) -> &str {
self.etag.as_ref()
}
#[inline]
pub fn last_modified(&self) -> String {
httpdate::fmt_http_date(self.last_modified)
}
#[inline]
pub fn cache_control(&self) -> String {
self.cache_control.into()
}
}
#[cfg(feature = "middleware")]
impl App {
pub fn with_cache_control<F>(mut self, config: F) -> Self
where
F: Fn(CacheControl) -> CacheControl,
{
self.cache_control = Some(config(CacheControl::default()));
self
}
}
#[cfg(feature = "middleware")]
impl<'a> Route<'a> {
pub fn cache_control<F>(self, config: F) -> Self
where
F: Fn(CacheControl) -> CacheControl,
{
let hv: HeaderValue = config(CacheControl::default())
.try_into()
.expect("valid cache-control header");
self.map_ok(move |resp: HttpResponse| {
make_cache_control_fn(resp, hv.clone(), HeaderInsertMode::Override)
})
}
}
#[cfg(feature = "middleware")]
impl<'a> RouteGroup<'a> {
pub fn cache_control<F>(&mut self, config: F) -> &mut Self
where
F: Fn(CacheControl) -> CacheControl,
{
let hv: HeaderValue = config(CacheControl::default())
.try_into()
.expect("valid cache-control header");
self.map_ok(move |resp: HttpResponse| {
make_cache_control_fn(resp, hv.clone(), HeaderInsertMode::IfAbsent)
})
}
}
#[cfg(feature = "middleware")]
#[derive(Copy, Clone)]
enum HeaderInsertMode {
Override,
IfAbsent,
}
#[cfg(feature = "middleware")]
#[inline]
fn make_cache_control_fn(
mut resp: HttpResponse,
hv: HeaderValue,
mode: HeaderInsertMode,
) -> impl Future<Output = HttpResult> {
match mode {
HeaderInsertMode::Override => {
resp.headers_mut().insert(CACHE_CONTROL, hv);
}
HeaderInsertMode::IfAbsent => {
if !resp.headers().contains_key(CACHE_CONTROL) {
resp.headers_mut().insert(CACHE_CONTROL, hv);
}
}
}
futures_util::future::ready(Ok(resp))
}
#[cfg(test)]
mod tests {
use crate::headers::{CacheControl, ETag, Header, ResponseCaching};
use std::time::SystemTime;
#[test]
fn it_creates_cache_control_string() {
let cache_control = CacheControl {
max_age: 60.into(),
public: true,
must_revalidate: false,
proxy_revalidate: true,
no_store: true,
no_cache: false,
s_max_age: 60.into(),
..Default::default()
};
assert_eq!(
"no-store, max-age=60, s-maxage=60, proxy-revalidate, public",
cache_control.to_string()
);
}
#[test]
fn if_returns_etag() {
let caching = ResponseCaching {
etag: ETag::strong("123"),
last_modified: SystemTime::now(),
cache_control: Default::default(),
};
assert_eq!(caching.etag(), "\"123\"");
}
#[test]
fn if_returns_last_modified_string() {
let now = SystemTime::now();
let caching = ResponseCaching {
etag: ETag::strong("123"),
last_modified: now,
cache_control: Default::default(),
};
assert_eq!(caching.last_modified(), httpdate::fmt_http_date(now));
}
#[test]
fn if_returns_cache_control_string() {
let cache_control = CacheControl {
max_age: 60.into(),
private: true,
immutable: true,
..Default::default()
};
let caching = ResponseCaching {
etag: ETag::strong("123"),
last_modified: SystemTime::now(),
cache_control,
};
assert_eq!(caching.cache_control(), "max-age=60, private, immutable");
}
#[test]
fn it_tests_no_store_clears_ages() {
let cc = CacheControl::default()
.with_max_age(300)
.with_s_max_age(120)
.with_no_store();
assert!(cc.no_store);
assert_eq!(cc.max_age, None);
assert_eq!(cc.s_max_age, None);
assert_eq!(cc.stale_if_error, None);
assert_eq!(cc.stale_while_revalidate, None);
}
#[test]
fn it_tests_public_private_conflict() {
let cc = CacheControl::default().with_private().with_public();
assert!(cc.public);
assert!(!cc.private);
}
#[test]
fn it_tests_immutable_conflicts() {
let cc = CacheControl::default()
.with_must_revalidate()
.with_proxy_revalidate()
.with_no_cache()
.with_immutable();
assert!(cc.immutable);
assert!(!cc.no_cache);
assert!(!cc.must_revalidate);
assert!(!cc.proxy_revalidate);
}
#[test]
fn it_tests_max_age_disables_no_store() {
let cc = CacheControl::default().with_no_store().with_max_age(600);
assert!(!cc.no_store);
assert_eq!(cc.max_age, Some(600));
}
#[test]
fn it_tests_combination() {
let cc = CacheControl::default()
.with_public()
.with_max_age(3600)
.with_immutable();
assert!(cc.public);
assert_eq!(cc.max_age, Some(3600));
assert!(cc.immutable);
}
#[test]
fn it_tests_stale_while_revalidate() {
let cc = CacheControl::default().with_stale_while_revalidate(600);
assert_eq!(cc.stale_while_revalidate, Some(600));
}
#[test]
fn it_tests_stale_if_error() {
let cc = CacheControl::default().with_stale_if_error(600);
assert_eq!(cc.stale_if_error, Some(600));
}
#[test]
fn it_converts_cache_control_to_header() {
let cc = CacheControl::default()
.with_no_cache()
.with_must_revalidate()
.with_max_age(0);
let header = Header::<CacheControl>::try_from(cc).unwrap();
assert_eq!(header.name(), "cache-control");
assert_eq!(
header.value().to_str().unwrap(),
"no-cache, max-age=0, must-revalidate"
)
}
}