use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime};
use bytes::Bytes;
use http::header::{AGE, ETAG, IF_MODIFIED_SINCE, IF_NONE_MATCH, LAST_MODIFIED};
use http::{HeaderMap, Method, StatusCode, Uri};
mod headers;
mod policy;
mod store;
#[cfg(any(not(target_arch = "wasm32"), test))]
pub(crate) use policy::is_response_cacheable;
pub use store::{CacheStore, InMemoryCacheStore};
#[cfg(test)]
use headers::httpdate_parse;
use headers::{parse_cache_control, parse_expires};
use policy::{is_cacheable_method, is_cacheable_status, is_unsafe_method, vary_matches};
#[derive(Clone)]
pub struct CacheEntry {
pub(crate) status: StatusCode,
pub(crate) headers: HeaderMap,
pub(crate) body: Bytes,
pub(crate) stored_at: Instant,
pub(crate) max_age: Option<Duration>,
pub(crate) expires_at: Option<SystemTime>,
pub(crate) etag: Option<String>,
pub(crate) last_modified: Option<String>,
pub(crate) must_revalidate: bool,
pub(crate) immutable: bool,
pub(crate) stale_while_revalidate: Option<Duration>,
pub(crate) stale_if_error: Option<Duration>,
pub(crate) vary: Option<Vec<String>>,
pub(crate) request_vary_headers: Option<Vec<(String, Option<String>)>>,
}
impl CacheEntry {
fn is_fresh(&self) -> bool {
if let Some(max_age) = self.max_age {
return self.stored_at.elapsed() < max_age;
}
if let Some(expires) = self.expires_at {
return SystemTime::now() < expires;
}
false
}
fn age(&self) -> Duration {
self.stored_at.elapsed()
}
fn staleness(&self) -> Option<Duration> {
let age = self.age();
if let Some(max_age) = self.max_age {
if age > max_age {
return Some(age - max_age);
}
return None;
}
if let Some(expires) = self.expires_at {
if let Ok(since_epoch) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)
&& let Ok(expires_since) = expires.duration_since(SystemTime::UNIX_EPOCH)
&& since_epoch > expires_since
{
return Some(since_epoch - expires_since);
}
return None;
}
None
}
fn has_validators(&self) -> bool {
self.etag.is_some() || self.last_modified.is_some()
}
}
#[derive(Clone, Debug)]
pub struct CacheConfig {
pub max_entries: usize,
}
impl Default for CacheConfig {
fn default() -> Self {
Self { max_entries: 256 }
}
}
pub struct HttpCache {
store: Arc<dyn CacheStore>,
}
impl Clone for HttpCache {
fn clone(&self) -> Self {
Self {
store: self.store.clone(),
}
}
}
impl std::fmt::Debug for HttpCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HttpCache").finish()
}
}
impl HttpCache {
pub fn new() -> Self {
Self::with_config(CacheConfig::default())
}
pub fn with_config(config: CacheConfig) -> Self {
Self {
store: Arc::new(InMemoryCacheStore::new(config.max_entries)),
}
}
pub fn with_store(store: impl CacheStore) -> Self {
Self {
store: Arc::new(store),
}
}
pub fn clear(&self) {
self.store.clear();
}
pub fn len(&self) -> usize {
self.store.len()
}
pub fn is_empty(&self) -> bool {
self.store.is_empty()
}
pub(crate) fn lookup(
&self,
method: &Method,
uri: &Uri,
request_headers: &HeaderMap,
) -> CacheLookup {
if !is_cacheable_method(method) {
return CacheLookup::Miss;
}
let entries = self.store.get(method, uri);
if entries.is_empty() {
return CacheLookup::Miss;
}
let entry = entries
.into_iter()
.find(|e| vary_matches(e, request_headers));
let Some(entry) = entry else {
return CacheLookup::Miss;
};
if entry.is_fresh() && !entry.must_revalidate {
return CacheLookup::Fresh(CachedResponse {
status: entry.status,
headers: entry.headers.clone(),
body: entry.body.clone(),
age: entry.age(),
});
}
if entry.immutable && entry.is_fresh() {
return CacheLookup::Fresh(CachedResponse {
status: entry.status,
headers: entry.headers.clone(),
body: entry.body.clone(),
age: entry.age(),
});
}
if let Some(swr) = entry.stale_while_revalidate
&& let Some(staleness) = entry.staleness()
&& staleness <= swr
{
return CacheLookup::Fresh(CachedResponse {
status: entry.status,
headers: entry.headers.clone(),
body: entry.body.clone(),
age: entry.age(),
});
}
if entry.has_validators() {
return CacheLookup::Stale {
validators: Validators {
etag: entry.etag.clone(),
last_modified: entry.last_modified.clone(),
},
cached: CachedResponse {
status: entry.status,
headers: entry.headers.clone(),
body: entry.body.clone(),
age: entry.age(),
},
stale_if_error: entry.stale_if_error,
};
}
CacheLookup::Miss
}
pub(crate) fn store(
&self,
method: &Method,
uri: &Uri,
status: StatusCode,
headers: &HeaderMap,
body: &Bytes,
request_headers: &HeaderMap,
) {
if !is_cacheable_method(method) || !is_cacheable_status(status) {
return;
}
let directives = parse_cache_control(headers);
if directives.no_store || directives.private {
return;
}
let has_validators = headers.contains_key(ETAG) || headers.contains_key(LAST_MODIFIED);
if directives.no_cache && !has_validators {
return;
}
let vary = headers
.get(http::header::VARY)
.and_then(|v| v.to_str().ok())
.map(|v| {
v.split(',')
.map(|s| s.trim().to_lowercase())
.collect::<Vec<_>>()
});
let request_vary_headers = vary.as_ref().map(|vary_names| {
vary_names
.iter()
.map(|name| {
let val = http::header::HeaderName::from_bytes(name.as_bytes())
.ok()
.and_then(|hn| request_headers.get(&hn))
.and_then(|v| v.to_str().ok())
.map(String::from);
(name.clone(), val)
})
.collect::<Vec<_>>()
});
let entry = CacheEntry {
status,
headers: headers.clone(),
body: body.clone(),
stored_at: Instant::now(),
max_age: directives.max_age,
expires_at: if directives.max_age.is_none() {
parse_expires(headers)
} else {
None
},
etag: headers
.get(ETAG)
.and_then(|v| v.to_str().ok())
.map(String::from),
last_modified: headers
.get(LAST_MODIFIED)
.and_then(|v| v.to_str().ok())
.map(String::from),
must_revalidate: directives.must_revalidate,
immutable: directives.immutable,
stale_while_revalidate: directives.stale_while_revalidate,
stale_if_error: directives.stale_if_error,
vary,
request_vary_headers,
};
self.store.put(method, uri, entry);
}
pub(crate) fn invalidate(&self, method: &Method, uri: &Uri) {
if is_unsafe_method(method) {
self.store.remove(&Method::GET, uri);
self.store.remove(&Method::HEAD, uri);
}
}
}
impl Default for HttpCache {
fn default() -> Self {
Self::new()
}
}
pub(crate) enum CacheLookup {
Fresh(CachedResponse),
Stale {
validators: Validators,
cached: CachedResponse,
stale_if_error: Option<Duration>,
},
Miss,
}
pub(crate) struct CachedResponse {
pub status: StatusCode,
pub headers: HeaderMap,
pub body: Bytes,
pub age: Duration,
}
pub(crate) struct Validators {
pub etag: Option<String>,
pub last_modified: Option<String>,
}
impl Validators {
pub fn apply_to_request(&self, headers: &mut HeaderMap) {
if let Some(ref etag) = self.etag
&& let Ok(val) = etag.parse()
{
headers.insert(IF_NONE_MATCH, val);
}
if let Some(ref lm) = self.last_modified
&& let Ok(val) = lm.parse()
{
headers.insert(IF_MODIFIED_SINCE, val);
}
}
}
impl CachedResponse {
pub fn into_http_response(self) -> http::Response<crate::body::RequestBodySend> {
use http_body_util::BodyExt;
let mut builder = http::Response::builder().status(self.status);
for (name, value) in &self.headers {
builder = builder.header(name, value);
}
if let Ok(age_secs) = http::HeaderValue::from_str(&self.age.as_secs().to_string()) {
builder = builder.header(AGE, age_secs);
}
#[allow(clippy::expect_used)]
builder
.body(
http_body_util::Full::new(self.body)
.map_err(|never| match never {})
.boxed_unsync(),
)
.expect("cached response build should not fail")
}
}
#[cfg(test)]
mod tests;