Skip to main content

rust_web_server/cache/
mod.rs

1//! In-memory response cache middleware.
2//!
3//! [`CacheLayer`] implements [`Middleware`] and short-circuits the inner
4//! application for cacheable `GET` responses within their TTL.  Entries are
5//! bounded by a configurable capacity; the oldest entry is evicted when the
6//! store is full and no expired entries remain.
7//!
8//! # What is cached
9//!
10//! - Method: **GET only**; all other methods bypass the cache.
11//! - Status: 2xx responses (200, 201, 203, 204, 206, …).
12//! - Response `Cache-Control: no-store` or `private` — **not** cached.
13//! - Private or partial requests — `Authorization`, `Cookie` or `Range` — skip
14//!   the cache entirely (no lookup, no store): their responses may differ per
15//!   user or cover only part of the resource.
16//! - Responses that set a cookie (`Set-Cookie`) or stream their body
17//!   (`stream_file` / `stream_pipe`, not held in memory) are **not** cached.
18//! - Request `Cache-Control: no-cache` — cache is bypassed, handler is called,
19//!   but the fresh response **is** stored (revalidation).
20//!
21//! # Example
22//!
23//! ```rust,no_run
24//! use rust_web_server::app::App;
25//! use rust_web_server::cache::CacheLayer;
26//! use rust_web_server::core::New;
27//!
28//! let app = App::new()
29//!     .wrap(CacheLayer::memory(1000).ttl(60).vary_by_header("Accept"));
30//! ```
31
32#[cfg(test)]
33mod tests;
34
35use std::collections::{HashMap, VecDeque};
36use std::sync::atomic::{AtomicU64, Ordering};
37use std::sync::{Arc, Mutex};
38use std::time::{Duration, Instant};
39
40use crate::application::Application;
41use crate::header::Header;
42use crate::middleware::Middleware;
43use crate::request::{METHOD, Request};
44use crate::response::Response;
45use crate::server::ConnectionInfo;
46
47// ── cache store ───────────────────────────────────────────────────────────────
48
49struct CachedEntry {
50    response: Response,
51    inserted_at: Instant,
52}
53
54struct CacheStore {
55    entries: HashMap<String, CachedEntry>,
56    /// Insertion order — front is oldest; used for capacity eviction.
57    order: VecDeque<String>,
58}
59
60impl CacheStore {
61    fn new() -> Self {
62        CacheStore { entries: HashMap::new(), order: VecDeque::new() }
63    }
64
65    fn get(&self, key: &str, ttl: Duration) -> Option<&CachedEntry> {
66        self.entries.get(key).filter(|e| e.inserted_at.elapsed() < ttl)
67    }
68
69    fn insert(&mut self, key: String, entry: CachedEntry, capacity: usize) {
70        // Update in place without disturbing insertion order.
71        if self.entries.contains_key(&key) {
72            self.entries.insert(key, entry);
73            return;
74        }
75        // `purge_expired` is called by the caller before `insert`, so any
76        // remaining entries are still live. Evict the oldest if we're full.
77        if self.entries.len() >= capacity {
78            if let Some(oldest) = self.order.pop_front() {
79                self.entries.remove(&oldest);
80            }
81        }
82        self.order.push_back(key.clone());
83        self.entries.insert(key, entry);
84    }
85
86    fn purge_expired(&mut self, ttl: Duration) {
87        let expired: Vec<String> = self.entries.iter()
88            .filter(|(_, e)| e.inserted_at.elapsed() >= ttl)
89            .map(|(k, _)| k.clone())
90            .collect();
91        for k in &expired {
92            self.entries.remove(k);
93            self.order.retain(|o| o != k);
94        }
95    }
96}
97
98// ── CacheLayer ────────────────────────────────────────────────────────────────
99
100/// An in-memory response cache middleware.
101///
102/// Construct with [`CacheLayer::memory`] and configure with the builder methods
103/// [`ttl`](CacheLayer::ttl) and [`vary_by_header`](CacheLayer::vary_by_header).
104///
105/// `CacheLayer` is cheaply `Clone`-able — clones share the same underlying
106/// store, which lets you keep a handle for cache stats and invalidation while
107/// the other clone is used as middleware.
108#[derive(Clone)]
109pub struct CacheLayer {
110    store: Arc<Mutex<CacheStore>>,
111    hits: Arc<AtomicU64>,
112    misses: Arc<AtomicU64>,
113    capacity: usize,
114    ttl: Duration,
115    vary_headers: Vec<String>,
116}
117
118impl CacheLayer {
119    /// Create a new in-memory cache bounded to `capacity` entries.
120    ///
121    /// Default TTL is **60 seconds**. Adjust with [`.ttl()`](CacheLayer::ttl).
122    pub fn memory(capacity: usize) -> Self {
123        CacheLayer {
124            store: Arc::new(Mutex::new(CacheStore::new())),
125            hits: Arc::new(AtomicU64::new(0)),
126            misses: Arc::new(AtomicU64::new(0)),
127            capacity,
128            ttl: Duration::from_secs(60),
129            vary_headers: vec![],
130        }
131    }
132
133    /// Number of cache hits since the cache was created.
134    pub fn hits(&self) -> u64 {
135        self.hits.load(Ordering::Relaxed)
136    }
137
138    /// Number of cache misses since the cache was created.
139    pub fn misses(&self) -> u64 {
140        self.misses.load(Ordering::Relaxed)
141    }
142
143    /// Current number of entries in the store (including potentially expired ones
144    /// that haven't been purged yet).
145    pub fn size(&self) -> usize {
146        self.store.lock().unwrap().entries.len()
147    }
148
149    /// Remove all entries from the cache.
150    pub fn clear(&self) {
151        let mut guard = self.store.lock().unwrap();
152        guard.entries.clear();
153        guard.order.clear();
154    }
155
156    /// Set the time-to-live for cached entries.
157    pub fn ttl(mut self, secs: u64) -> Self {
158        self.ttl = Duration::from_secs(secs);
159        self
160    }
161
162    /// Include a request header in the cache key so that different values of
163    /// that header produce separate cache entries.
164    ///
165    /// Header name matching is case-insensitive. Call multiple times to vary
166    /// by more than one header.
167    ///
168    /// ```rust,no_run
169    /// use rust_web_server::cache::CacheLayer;
170    ///
171    /// let layer = CacheLayer::memory(500)
172    ///     .vary_by_header("Accept")
173    ///     .vary_by_header("Accept-Language");
174    /// ```
175    pub fn vary_by_header(mut self, name: &str) -> Self {
176        self.vary_headers.push(name.to_ascii_lowercase());
177        self
178    }
179
180    fn store(&self) -> &Mutex<CacheStore> {
181        &self.store
182    }
183
184    /// Build a cache key from the request URI and any configured vary headers.
185    fn cache_key(&self, request: &Request) -> String {
186        let mut key = request.request_uri.clone();
187        for vh in &self.vary_headers {
188            let val = request.headers.iter()
189                .find(|h| h.name.eq_ignore_ascii_case(vh))
190                .map(|h| h.value.as_str())
191                .unwrap_or("");
192            key.push('\x00');
193            key.push_str(val);
194        }
195        key
196    }
197
198    /// `true` when the request carries `Cache-Control: no-cache`, meaning the
199    /// client wants a fresh response (but we may still store the result).
200    fn request_bypasses_cache(request: &Request) -> bool {
201        request.headers.iter().any(|h| {
202            h.name.eq_ignore_ascii_case(Header::_CACHE_CONTROL)
203                && h.value.to_ascii_lowercase().contains("no-cache")
204        })
205    }
206
207    /// `true` for a request whose response may be private to the client
208    /// (credentials, cookies) or partial (`Range`): never answered from, nor
209    /// stored in, the shared cache (#49).
210    fn request_is_private(request: &Request) -> bool {
211        request.headers.iter().any(|h| {
212            h.name.eq_ignore_ascii_case(Header::_AUTHORIZATION)
213                || h.name.eq_ignore_ascii_case(Header::_COOKIE)
214                || h.name.eq_ignore_ascii_case(Header::_RANGE)
215        })
216    }
217
218    /// `true` when the response may be stored in the cache.
219    fn response_is_cacheable(response: &Response) -> bool {
220        if response.status_code < 200 || response.status_code >= 300 {
221            return false;
222        }
223        // A streamed body isn't held in the response: a stored copy would
224        // replay an empty body. A cookie-setting response is per client.
225        if response.stream_file.is_some() || response.stream_pipe.is_some() {
226            return false;
227        }
228        if response.headers.iter().any(|h| h.name.eq_ignore_ascii_case(Header::_SET_COOKIE)) {
229            return false;
230        }
231        !response.headers.iter().any(|h| {
232            if !h.name.eq_ignore_ascii_case(Header::_CACHE_CONTROL) {
233                return false;
234            }
235            let v = h.value.to_ascii_lowercase();
236            v.contains("no-store") || v.contains("private")
237        })
238    }
239
240    /// Age of the entry in whole seconds, capped at u64::MAX.
241    fn age_secs(entry: &CachedEntry) -> u64 {
242        entry.inserted_at.elapsed().as_secs()
243    }
244
245    /// Build a response from a cache hit, injecting an `Age` header.
246    fn cached_response(entry: &CachedEntry) -> Response {
247        let mut response = entry.response.clone();
248        let age = Self::age_secs(entry);
249        // Replace or add the Age header.
250        if let Some(h) = response.headers.iter_mut().find(|h| h.name.eq_ignore_ascii_case("Age")) {
251            h.value = age.to_string();
252        } else {
253            response.headers.push(Header { name: "Age".to_string(), value: age.to_string() });
254        }
255        response
256    }
257}
258
259impl Middleware for CacheLayer {
260    fn handle(
261        &self,
262        request: &Request,
263        connection: &ConnectionInfo,
264        next: &dyn Application,
265    ) -> Result<Response, String> {
266        // Only cache GET requests, and never private or partial ones.
267        if request.method != METHOD.get || Self::request_is_private(request) {
268            return next.execute(request, connection);
269        }
270
271        let key = self.cache_key(request);
272        let bypass = Self::request_bypasses_cache(request);
273
274        if !bypass {
275            // Check for a valid cache hit.
276            let guard = self.store().lock().unwrap();
277            if let Some(entry) = guard.get(&key, self.ttl) {
278                self.hits.fetch_add(1, Ordering::Relaxed);
279                return Ok(Self::cached_response(entry));
280            }
281        }
282        self.misses.fetch_add(1, Ordering::Relaxed);
283
284        // Cache miss (or bypass): call the handler.
285        let response = next.execute(request, connection)?;
286
287        if Self::response_is_cacheable(&response) {
288            let mut guard = self.store().lock().unwrap();
289            guard.purge_expired(self.ttl);
290            guard.insert(
291                key,
292                CachedEntry { response: response.clone(), inserted_at: Instant::now() },
293                self.capacity,
294            );
295        }
296
297        Ok(response)
298    }
299
300    /// In-memory only: safe on the event loop.
301    fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
302        true
303    }
304}