1use crate::error::Result;
55use crate::middleware::{Middleware, Next};
56use crate::request::Request;
57use crate::response::Response;
58use async_trait::async_trait;
59use bytes::Bytes;
60use http::StatusCode;
61use std::collections::HashMap;
62use std::hash::{BuildHasher, Hash, Hasher};
63use std::sync::{Arc, Mutex};
64use std::time::{Duration, Instant};
65use tokio::sync::watch;
66
67struct Stored {
68 status: StatusCode,
69 headers: http::HeaderMap,
70 body: Bytes,
71}
72
73impl Stored {
74 fn to_response(&self, state: &str, age: Duration) -> Response {
75 let mut res = Response::new(self.status).with_body(self.body.clone());
76 res.headers = self.headers.clone();
77 res.with_header("x-cache", state)
78 .with_header("age", age.as_secs().to_string())
79 }
80}
81
82struct Entry {
83 stored: Arc<Stored>,
84 stored_at: Instant,
85 fresh_for: Duration,
86}
87
88#[derive(Clone)]
89enum Flight {
90 Pending,
91 Done(Option<Arc<Stored>>),
92}
93
94#[derive(Default)]
95struct Slot {
96 path: String,
97 entry: Option<Entry>,
98 flight: Option<watch::Receiver<Flight>>,
99 refreshing: bool,
100}
101
102struct Inner {
103 slots: Mutex<HashMap<String, Slot>>,
104 hasher: std::collections::hash_map::RandomState,
105}
106
107#[derive(Clone)]
110pub struct ResponseCache {
111 inner: Arc<Inner>,
112 ttl: Duration,
113 swr: Duration,
114 max_entries: usize,
115 max_body: usize,
116 statuses: Vec<u16>,
117 vary: Vec<String>,
118 per_user: bool,
119}
120
121impl Default for ResponseCache {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127impl ResponseCache {
128 pub fn new() -> Self {
129 Self {
130 inner: Arc::new(Inner {
131 slots: Mutex::new(HashMap::new()),
132 hasher: Default::default(),
133 }),
134 ttl: Duration::from_secs(60),
135 swr: Duration::ZERO,
136 max_entries: 1_000,
137 max_body: 1024 * 1024,
138 statuses: vec![200],
139 vary: Vec::new(),
140 per_user: false,
141 }
142 }
143
144 pub fn ttl(mut self, ttl: Duration) -> Self {
146 self.ttl = ttl;
147 self
148 }
149
150 pub fn stale_while_revalidate(mut self, window: Duration) -> Self {
153 self.swr = window;
154 self
155 }
156
157 pub fn max_entries(mut self, max: usize) -> Self {
159 self.max_entries = max.max(1);
160 self
161 }
162
163 pub fn max_body(mut self, bytes: usize) -> Self {
165 self.max_body = bytes;
166 self
167 }
168
169 pub fn statuses<I: IntoIterator<Item = u16>>(mut self, statuses: I) -> Self {
171 self.statuses = statuses.into_iter().collect();
172 self
173 }
174
175 pub fn vary_by<I, S>(mut self, headers: I) -> Self
178 where
179 I: IntoIterator<Item = S>,
180 S: Into<String>,
181 {
182 self.vary = headers
183 .into_iter()
184 .map(|h| h.into().to_ascii_lowercase())
185 .collect();
186 self
187 }
188
189 pub fn per_user(mut self) -> Self {
193 self.per_user = true;
194 self
195 }
196
197 pub fn invalidate_prefix(&self, prefix: &str) {
199 let mut map = self.lock();
200 map.retain(|_, slot| !slot.path.starts_with(prefix));
201 }
202
203 pub fn clear(&self) {
205 self.lock().clear();
206 }
207
208 pub fn len(&self) -> usize {
210 self.lock().values().filter(|s| s.entry.is_some()).count()
211 }
212
213 pub fn is_empty(&self) -> bool {
214 self.len() == 0
215 }
216
217 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Slot>> {
218 self.inner.slots.lock().unwrap_or_else(|e| e.into_inner())
219 }
220
221 fn key(&self, req: &Request, scope: bool) -> String {
222 let mut key = req.uri.path().to_string();
223 if let Some(q) = req.uri.query() {
224 key.push('?');
225 key.push_str(q);
226 }
227 for name in &self.vary {
228 key.push('|');
229 if let Some(v) = req.headers.get(name.as_str()).and_then(|v| v.to_str().ok()) {
230 key.push_str(v);
231 }
232 }
233 if scope {
234 let mut h = self.inner.hasher.build_hasher();
235 for name in ["authorization", "cookie"] {
236 req.headers.get(name).map(|v| v.as_bytes()).hash(&mut h);
237 }
238 key.push_str(&format!("|u{:016x}", h.finish()));
239 }
240 key
241 }
242
243 fn lifetime(&self, res: &Response) -> Option<Duration> {
245 if !self.statuses.contains(&res.status.as_u16())
246 || res.is_streaming()
247 || res.has_cookies()
248 || res.headers.contains_key(http::header::SET_COOKIE)
249 || res.body.len() > self.max_body
250 {
251 return None;
252 }
253 let mut max_age = None;
254 if let Some(cc) = res
255 .headers
256 .get(http::header::CACHE_CONTROL)
257 .and_then(|v| v.to_str().ok())
258 {
259 for directive in cc.split(',').map(|d| d.trim().to_ascii_lowercase()) {
260 match directive.as_str() {
261 "no-store" | "private" | "no-cache" => return None,
262 d => {
263 if let Some(n) = d
264 .strip_prefix("s-maxage=")
265 .or_else(|| d.strip_prefix("max-age="))
266 {
267 if let Ok(secs) = n.trim_matches('"').parse::<u64>() {
268 max_age.get_or_insert(secs);
269 if d.starts_with("s-maxage=") {
270 max_age = Some(secs);
271 }
272 }
273 }
274 }
275 }
276 }
277 }
278 match max_age {
279 Some(0) => None,
280 Some(secs) => Some(Duration::from_secs(secs)),
281 None => Some(self.ttl),
282 }
283 }
284
285 fn snapshot(&self, res: &Response) -> Stored {
286 let mut headers = res.headers.clone();
287 headers.remove(http::header::AGE);
289 headers.remove("x-cache");
290 Stored {
291 status: res.status,
292 headers,
293 body: res.body.clone(),
294 }
295 }
296
297 fn store(&self, key: &str, path: &str, stored: Arc<Stored>, fresh_for: Duration) {
298 let mut map = self.lock();
299 let others = |map: &HashMap<String, Slot>| map.len() - usize::from(map.contains_key(key));
300 if others(&map) >= self.max_entries {
301 let now = Instant::now();
302 let swr = self.swr;
303 map.retain(|k, s| {
304 k == key
305 || s.flight.is_some()
306 || s.entry
307 .as_ref()
308 .is_some_and(|e| now.duration_since(e.stored_at) < e.fresh_for + swr)
309 });
310 while others(&map) >= self.max_entries {
311 let oldest = map
312 .iter()
313 .filter(|(k, s)| *k != key && s.flight.is_none() && s.entry.is_some())
314 .min_by_key(|(_, s)| s.entry.as_ref().map(|e| e.stored_at))
315 .map(|(k, _)| k.clone());
316 match oldest {
317 Some(k) => {
318 map.remove(&k);
319 }
320 None => break,
321 }
322 }
323 }
324 let slot = map.entry(key.to_string()).or_default();
325 slot.path = path.to_string();
326 slot.entry = Some(Entry {
327 stored,
328 stored_at: Instant::now(),
329 fresh_for,
330 });
331 }
332
333 fn finish_flight(&self, key: &str) {
334 let mut map = self.lock();
335 if let Some(slot) = map.get_mut(key) {
336 slot.flight = None;
337 if slot.entry.is_none() {
338 map.remove(key);
339 }
340 }
341 }
342
343 fn end_refresh(&self, key: &str) {
344 let mut map = self.lock();
345 if let Some(slot) = map.get_mut(key) {
346 slot.refreshing = false;
347 if slot.entry.is_none() && slot.flight.is_none() {
348 map.remove(key);
349 }
350 }
351 }
352}
353
354struct Leader {
356 cache: ResponseCache,
357 key: String,
358 tx: watch::Sender<Flight>,
359 done: bool,
360}
361
362impl Leader {
363 fn finish(mut self, result: Option<Arc<Stored>>) {
364 self.done = true;
365 let _ = self.tx.send(Flight::Done(result));
366 self.cache.finish_flight(&self.key);
367 }
368}
369
370impl Drop for Leader {
371 fn drop(&mut self) {
372 if !self.done {
373 let _ = self.tx.send(Flight::Done(None));
374 self.cache.finish_flight(&self.key);
375 }
376 }
377}
378
379enum Plan {
380 Hit(Arc<Stored>, Duration),
381 Stale(Arc<Stored>, Duration, bool),
382 Wait(watch::Receiver<Flight>),
383 Lead(Leader),
384}
385
386impl ResponseCache {
387 fn plan(&self, key: &str, path: &str) -> Plan {
388 let mut map = self.lock();
389 let slot = map.entry(key.to_string()).or_default();
390 if slot.path.is_empty() {
391 slot.path = path.to_string();
392 }
393 if let Some(entry) = &slot.entry {
394 let age = entry.stored_at.elapsed();
395 if age < entry.fresh_for {
396 return Plan::Hit(entry.stored.clone(), age);
397 }
398 if age < entry.fresh_for + self.swr {
399 let start_refresh = !slot.refreshing && slot.flight.is_none();
400 if start_refresh {
401 slot.refreshing = true;
402 }
403 return Plan::Stale(entry.stored.clone(), age, start_refresh);
404 }
405 }
406 if let Some(rx) = &slot.flight {
407 return Plan::Wait(rx.clone());
408 }
409 let (tx, rx) = watch::channel(Flight::Pending);
410 slot.flight = Some(rx);
411 slot.entry = None;
412 Plan::Lead(Leader {
413 cache: self.clone(),
414 key: key.to_string(),
415 tx,
416 done: false,
417 })
418 }
419}
420
421#[async_trait]
422impl Middleware for ResponseCache {
423 async fn handle(&self, req: Request, next: Next) -> Result<Response> {
424 let credentialed = req.headers.contains_key(http::header::AUTHORIZATION)
425 || req.headers.contains_key(http::header::COOKIE);
426 if req.method != http::Method::GET || (credentialed && !self.per_user) {
427 return next.run(req).await;
428 }
429 let key = self.key(&req, credentialed);
430 let path = req.uri.path().to_string();
431 let finish = |mut res: Response| {
432 for v in &self.vary {
433 crate::cors::add_vary(&mut res, v);
434 }
435 res
436 };
437
438 let mut waited = false;
441 loop {
442 match self.plan(&key, &path) {
443 Plan::Hit(stored, age) => return Ok(finish(stored.to_response("HIT", age))),
444 Plan::Stale(stored, age, refresh) => {
445 if refresh {
446 self.spawn_refresh(key, path, next, req.duplicate());
447 }
448 return Ok(finish(stored.to_response("STALE", age)));
449 }
450 Plan::Wait(_) if waited => return next.run(req).await,
451 Plan::Wait(mut rx) => {
452 let outcome = loop {
453 if let Flight::Done(result) = &*rx.borrow_and_update() {
454 break Some(result.clone());
455 }
456 if rx.changed().await.is_err() {
457 break None;
458 }
459 };
460 match outcome.flatten() {
461 Some(stored) => {
462 return Ok(finish(stored.to_response("COALESCED", Duration::ZERO)))
463 }
464 None => waited = true,
465 }
466 }
467 Plan::Lead(leader) => {
468 let res = next.run(req).await.unwrap_or_else(Response::from_error);
469 return Ok(match self.lifetime(&res) {
470 Some(fresh_for) => {
471 let stored = Arc::new(self.snapshot(&res));
472 self.store(&key, &path, stored.clone(), fresh_for);
473 leader.finish(Some(stored));
474 finish(res.with_header("x-cache", "MISS"))
475 }
476 None => {
477 leader.finish(None);
478 res.with_header("x-cache", "BYPASS")
479 }
480 });
481 }
482 }
483 }
484 }
485}
486
487impl ResponseCache {
488 fn spawn_refresh(&self, key: String, path: String, next: Next, req: Request) {
490 let cache = self.clone();
491 tokio::spawn(async move {
492 let res = next.run(req).await.unwrap_or_else(Response::from_error);
493 match cache.lifetime(&res) {
494 Some(fresh_for) => {
495 let stored = Arc::new(cache.snapshot(&res));
496 cache.store(&key, &path, stored, fresh_for);
497 }
498 None => {
499 if let Some(slot) = cache.lock().get_mut(&key) {
501 slot.entry = None;
502 }
503 }
504 }
505 cache.end_refresh(&key);
506 });
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 fn res(status: u16, cc: Option<&str>) -> Response {
515 let mut r = Response::new(StatusCode::from_u16(status).unwrap()).with_body("x");
516 if let Some(cc) = cc {
517 r = r.with_header("cache-control", cc);
518 }
519 r
520 }
521
522 #[test]
523 fn lifetime_follows_cache_control() {
524 let c = ResponseCache::new().ttl(Duration::from_secs(60));
525 assert_eq!(c.lifetime(&res(200, None)), Some(Duration::from_secs(60)));
526 assert_eq!(
527 c.lifetime(&res(200, Some("max-age=5"))),
528 Some(Duration::from_secs(5))
529 );
530 assert_eq!(
531 c.lifetime(&res(200, Some("public, max-age=5, s-maxage=9"))),
532 Some(Duration::from_secs(9))
533 );
534 for no in ["no-store", "private", "no-cache", "max-age=0"] {
535 assert_eq!(c.lifetime(&res(200, Some(no))), None, "{no}");
536 }
537 assert_eq!(c.lifetime(&res(404, None)), None);
538 assert_eq!(
539 c.lifetime(&res(200, None).with_header("set-cookie", "a=b")),
540 None
541 );
542 }
543
544 #[test]
545 fn big_bodies_are_not_stored() {
546 let c = ResponseCache::new().max_body(1);
547 assert_eq!(c.lifetime(&Response::ok().with_body("xx")), None);
548 }
549
550 #[test]
551 fn statuses_are_configurable() {
552 let c = ResponseCache::new().statuses([200, 404]);
553 assert!(c.lifetime(&res(404, None)).is_some());
554 }
555}