1use crate::{HttpRequest, HttpResponse};
45use std::collections::{HashMap, HashSet, VecDeque};
46use std::fmt;
47use std::sync::{Arc, Mutex};
48use std::time::{Duration, Instant, SystemTime};
49use tokio::sync::RwLock;
50
51#[derive(Debug, Clone, PartialEq)]
57pub enum CacheDirective {
58 Public,
60 Private,
62 NoStore,
64 NoCache,
66 MaxAge(u64),
68 SMaxAge(u64),
70 MustRevalidate,
72 ProxyRevalidate,
74 NoTransform,
76 Immutable,
78 MaxStale(Option<u64>),
80 MinFresh(u64),
82 OnlyIfCached,
84 Extension(String, Option<String>),
86}
87
88impl CacheDirective {
89 pub fn parse(s: &str) -> Option<Self> {
91 let s = s.trim().to_lowercase();
92
93 if let Some((key, value)) = s.split_once('=') {
95 let key = key.trim();
96 let value = value.trim().trim_matches('"');
97
98 return match key {
99 "max-age" => value.parse().ok().map(CacheDirective::MaxAge),
100 "s-maxage" => value.parse().ok().map(CacheDirective::SMaxAge),
101 "max-stale" => Some(CacheDirective::MaxStale(value.parse().ok())),
102 "min-fresh" => value.parse().ok().map(CacheDirective::MinFresh),
103 _ => Some(CacheDirective::Extension(
104 key.to_string(),
105 Some(value.to_string()),
106 )),
107 };
108 }
109
110 match s.as_str() {
112 "public" => Some(CacheDirective::Public),
113 "private" => Some(CacheDirective::Private),
114 "no-store" => Some(CacheDirective::NoStore),
115 "no-cache" => Some(CacheDirective::NoCache),
116 "must-revalidate" => Some(CacheDirective::MustRevalidate),
117 "proxy-revalidate" => Some(CacheDirective::ProxyRevalidate),
118 "no-transform" => Some(CacheDirective::NoTransform),
119 "immutable" => Some(CacheDirective::Immutable),
120 "max-stale" => Some(CacheDirective::MaxStale(None)),
121 "only-if-cached" => Some(CacheDirective::OnlyIfCached),
122 _ => Some(CacheDirective::Extension(s, None)),
123 }
124 }
125
126 pub fn to_header_value(&self) -> String {
128 match self {
129 CacheDirective::Public => "public".to_string(),
130 CacheDirective::Private => "private".to_string(),
131 CacheDirective::NoStore => "no-store".to_string(),
132 CacheDirective::NoCache => "no-cache".to_string(),
133 CacheDirective::MaxAge(secs) => format!("max-age={}", secs),
134 CacheDirective::SMaxAge(secs) => format!("s-maxage={}", secs),
135 CacheDirective::MustRevalidate => "must-revalidate".to_string(),
136 CacheDirective::ProxyRevalidate => "proxy-revalidate".to_string(),
137 CacheDirective::NoTransform => "no-transform".to_string(),
138 CacheDirective::Immutable => "immutable".to_string(),
139 CacheDirective::MaxStale(Some(secs)) => format!("max-stale={}", secs),
140 CacheDirective::MaxStale(None) => "max-stale".to_string(),
141 CacheDirective::MinFresh(secs) => format!("min-fresh={}", secs),
142 CacheDirective::OnlyIfCached => "only-if-cached".to_string(),
143 CacheDirective::Extension(key, Some(value)) => format!("{}={}", key, value),
144 CacheDirective::Extension(key, None) => key.clone(),
145 }
146 }
147}
148
149#[derive(Debug, Clone, Default)]
181pub struct CacheControl {
182 pub directives: Vec<CacheDirective>,
184}
185
186impl CacheControl {
187 pub fn new() -> Self {
189 Self::default()
190 }
191
192 pub fn parse(header: &str) -> Self {
194 let directives: Vec<CacheDirective> = header
195 .split(',')
196 .filter_map(|s| CacheDirective::parse(s.trim()))
197 .collect();
198
199 Self { directives }
200 }
201
202 pub fn to_header_value(&self) -> String {
204 self.directives
205 .iter()
206 .map(|d| d.to_header_value())
207 .collect::<Vec<_>>()
208 .join(", ")
209 }
210
211 pub fn public(mut self) -> Self {
215 self.directives.push(CacheDirective::Public);
216 self
217 }
218
219 pub fn private(mut self) -> Self {
221 self.directives.push(CacheDirective::Private);
222 self
223 }
224
225 pub fn no_store(mut self) -> Self {
227 self.directives.push(CacheDirective::NoStore);
228 self
229 }
230
231 pub fn no_cache(mut self) -> Self {
233 self.directives.push(CacheDirective::NoCache);
234 self
235 }
236
237 pub fn max_age(mut self, duration: Duration) -> Self {
239 self.directives
240 .push(CacheDirective::MaxAge(duration.as_secs()));
241 self
242 }
243
244 pub fn s_maxage(mut self, duration: Duration) -> Self {
246 self.directives
247 .push(CacheDirective::SMaxAge(duration.as_secs()));
248 self
249 }
250
251 pub fn must_revalidate(mut self) -> Self {
253 self.directives.push(CacheDirective::MustRevalidate);
254 self
255 }
256
257 pub fn proxy_revalidate(mut self) -> Self {
259 self.directives.push(CacheDirective::ProxyRevalidate);
260 self
261 }
262
263 pub fn no_transform(mut self) -> Self {
265 self.directives.push(CacheDirective::NoTransform);
266 self
267 }
268
269 pub fn immutable(mut self) -> Self {
271 self.directives.push(CacheDirective::Immutable);
272 self
273 }
274
275 pub fn directive(mut self, directive: CacheDirective) -> Self {
277 self.directives.push(directive);
278 self
279 }
280
281 pub fn is_public(&self) -> bool {
285 self.directives
286 .iter()
287 .any(|d| matches!(d, CacheDirective::Public))
288 }
289
290 pub fn is_private(&self) -> bool {
292 self.directives
293 .iter()
294 .any(|d| matches!(d, CacheDirective::Private))
295 }
296
297 pub fn is_no_store(&self) -> bool {
299 self.directives
300 .iter()
301 .any(|d| matches!(d, CacheDirective::NoStore))
302 }
303
304 pub fn is_no_cache(&self) -> bool {
306 self.directives
307 .iter()
308 .any(|d| matches!(d, CacheDirective::NoCache))
309 }
310
311 pub fn is_must_revalidate(&self) -> bool {
313 self.directives
314 .iter()
315 .any(|d| matches!(d, CacheDirective::MustRevalidate))
316 }
317
318 pub fn is_immutable(&self) -> bool {
320 self.directives
321 .iter()
322 .any(|d| matches!(d, CacheDirective::Immutable))
323 }
324
325 pub fn get_max_age(&self) -> Option<u64> {
327 self.directives.iter().find_map(|d| match d {
328 CacheDirective::MaxAge(secs) => Some(*secs),
329 _ => None,
330 })
331 }
332
333 pub fn get_s_maxage(&self) -> Option<u64> {
335 self.directives.iter().find_map(|d| match d {
336 CacheDirective::SMaxAge(secs) => Some(*secs),
337 _ => None,
338 })
339 }
340
341 pub fn is_cacheable(&self) -> bool {
343 if self.is_no_store() {
345 return false;
346 }
347
348 self.is_public()
350 || self.is_private()
351 || self.get_max_age().is_some()
352 || self.get_s_maxage().is_some()
353 }
354
355 pub fn freshness_lifetime(&self) -> Option<u64> {
359 self.get_s_maxage().or_else(|| self.get_max_age())
360 }
361
362 pub fn never() -> Self {
366 Self::new().no_store().no_cache()
367 }
368
369 pub fn public_max_age(duration: Duration) -> Self {
371 Self::new().public().max_age(duration)
372 }
373
374 pub fn private_max_age(duration: Duration) -> Self {
376 Self::new().private().max_age(duration)
377 }
378
379 pub fn immutable_asset(duration: Duration) -> Self {
381 Self::new().public().max_age(duration).immutable()
382 }
383
384 pub fn revalidate(duration: Duration) -> Self {
386 Self::new().public().max_age(duration).must_revalidate()
387 }
388}
389
390impl fmt::Display for CacheControl {
391 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392 write!(f, "{}", self.to_header_value())
393 }
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Hash)]
402pub struct CacheKey {
403 pub method: crate::Method,
405 pub path: crate::ByteStr,
407 pub query: String,
418 pub vary_values: Vec<(String, String)>,
420 pub body_hash: Option<u64>,
424}
425
426impl CacheKey {
427 pub fn from_request(request: &HttpRequest) -> Self {
429 Self::from_request_with_vary(request, &[])
430 }
431
432 pub fn from_request_with_vary(request: &HttpRequest, vary_headers: &[&str]) -> Self {
434 use fmt::Write as _;
438 let mut query_params: Vec<_> = request.query().iter().collect();
439 query_params.sort_by(|a, b| a.0.cmp(b.0));
440 let mut query = String::new();
441 for (k, v) in &query_params {
442 let _ = write!(query, "{}:{}={}:{}&", k.len(), k, v.len(), v);
443 }
444
445 let mut vary_values: Vec<(String, String)> = vary_headers
447 .iter()
448 .filter_map(|header| {
449 request
452 .headers
453 .get(header)
454 .map(|v| (header.to_lowercase(), v.to_owned()))
455 })
456 .collect();
457 vary_values.sort_by(|a, b| a.0.cmp(&b.0));
458
459 let method = request.method.clone();
460
461 let body_hash = if method == "QUERY" {
464 use std::hash::{Hash, Hasher};
465 let mut hasher = std::hash::DefaultHasher::new();
466 request.body_bytes().hash(&mut hasher);
467 Some(hasher.finish())
468 } else {
469 None
470 };
471
472 Self {
473 method,
474 path: crate::ByteStr::from(request.path_only()),
477 query,
478 vary_values,
479 body_hash,
480 }
481 }
482
483 pub fn to_string_key(&self) -> String {
485 let vary_str = if self.vary_values.is_empty() {
486 String::new()
487 } else {
488 format!(
489 "|{}",
490 self.vary_values
491 .iter()
492 .map(|(k, v)| format!("{}:{}", k, v))
493 .collect::<Vec<_>>()
494 .join(",")
495 )
496 };
497
498 let body_str = self
499 .body_hash
500 .map(|h| format!("|body:{:016x}", h))
501 .unwrap_or_default();
502
503 if self.query.is_empty() {
504 format!("{}:{}{}{}", self.method, self.path, body_str, vary_str)
505 } else {
506 format!(
507 "{}:{}?{}{}{}",
508 self.method, self.path, self.query, body_str, vary_str
509 )
510 }
511 }
512}
513
514impl fmt::Display for CacheKey {
515 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
516 write!(f, "{}", self.to_string_key())
517 }
518}
519
520#[derive(Debug, Clone)]
526pub struct CachedResponse {
527 pub response: CachedResponseData,
529 pub cached_at: Instant,
531 pub expires_at: Instant,
533 pub etag: Option<String>,
535 pub last_modified: Option<SystemTime>,
537 pub vary: Vec<String>,
539 pub(crate) base_key: Option<String>,
547 pub(crate) eviction_seq: u64,
555}
556
557#[derive(Debug, Clone)]
559pub struct CachedResponseData {
560 pub status: u16,
562 pub headers: HashMap<String, String>,
564 pub body: bytes::Bytes,
566}
567
568impl CachedResponse {
569 pub fn new(response: &HttpResponse, ttl: Duration) -> Self {
571 let now = Instant::now();
572
573 let etag = response.headers.get("ETag").cloned();
574 let last_modified = response
575 .headers
576 .get("Last-Modified")
577 .and_then(|s| httpdate::parse_http_date(s).ok());
578 let vary = response
579 .headers
580 .get("Vary")
581 .map(|v| v.split(',').map(|s| s.trim().to_lowercase()).collect())
582 .unwrap_or_default();
583
584 Self {
585 response: CachedResponseData {
586 status: response.status,
587 headers: response.headers.clone().into(),
588 body: response.body.clone(),
589 },
590 cached_at: now,
591 expires_at: now + ttl,
592 etag,
593 last_modified,
594 vary,
595 base_key: None,
596 eviction_seq: 0,
597 }
598 }
599
600 pub fn is_fresh(&self) -> bool {
602 Instant::now() < self.expires_at
603 }
604
605 pub fn is_stale(&self) -> bool {
607 !self.is_fresh()
608 }
609
610 pub fn age(&self) -> Duration {
612 self.cached_at.elapsed()
613 }
614
615 pub fn remaining_ttl(&self) -> Option<Duration> {
617 let now = Instant::now();
618 if now < self.expires_at {
619 Some(self.expires_at - now)
620 } else {
621 None
622 }
623 }
624
625 pub fn to_response(&self) -> HttpResponse {
627 let mut response = HttpResponse::from_parts(
630 self.response.status,
631 self.response.headers.clone(),
632 Vec::new(),
633 )
634 .with_bytes_body(self.response.body.clone());
635
636 response
638 .headers
639 .insert("Age".to_string(), self.age().as_secs().to_string());
640
641 response
643 .headers
644 .insert("X-Cache".to_string(), "HIT".to_string());
645
646 response
647 }
648}
649
650#[derive(Debug)]
673pub struct ResponseCache {
674 config: ResponseCacheConfig,
676 entries: Arc<RwLock<HashMap<String, CachedResponse>>>,
678 vary_index: Arc<RwLock<HashMap<String, Vec<String>>>>,
682 eviction: Mutex<EvictionIndex>,
686}
687
688#[derive(Debug, Default)]
731struct EvictionIndex {
732 order: VecDeque<(String, u64)>,
733 base_key_counts: HashMap<String, usize>,
734 dead: usize,
738 next_seq: u64,
741}
742
743impl EvictionIndex {
744 fn next_seq(&mut self) -> u64 {
748 let seq = self.next_seq;
749 self.next_seq += 1;
750 seq
751 }
752
753 fn record_insert(&mut self, key: &str, seq: u64, base_key: &str, replaced: bool) {
758 self.order.push_back((key.to_string(), seq));
759 if replaced {
760 self.dead += 1;
761 } else {
762 *self
763 .base_key_counts
764 .entry(base_key.to_string())
765 .or_insert(0) += 1;
766 }
767 }
768
769 fn record_remove(&mut self, base_key: &str) -> bool {
778 if let Some(count) = self.base_key_counts.get_mut(base_key) {
779 *count -= 1;
780 if *count == 0 {
781 self.base_key_counts.remove(base_key);
782 return true;
783 }
784 }
785 false
786 }
787
788 fn base_key_live(&self, base_key: &str) -> bool {
790 self.base_key_counts.contains_key(base_key)
791 }
792
793 fn compact(&mut self, entries: &HashMap<String, CachedResponse>) {
802 let mut seen = HashSet::with_capacity(entries.len());
803 let mut compacted = VecDeque::with_capacity(entries.len());
804 for (key, seq) in self.order.drain(..) {
805 if entries.get(&key).is_some_and(|e| e.eviction_seq == seq) && seen.insert(key.clone())
806 {
807 compacted.push_back((key, seq));
808 }
809 }
810 self.order = compacted;
811 self.dead = 0;
812 }
813
814 fn compact_if_needed(&mut self, entries: &HashMap<String, CachedResponse>) {
819 if self.dead > entries.len() {
820 self.compact(entries);
821 }
822 }
823
824 fn clear(&mut self) {
825 self.order.clear();
826 self.base_key_counts.clear();
827 self.dead = 0;
828 }
829}
830
831#[derive(Debug, Clone)]
833pub struct ResponseCacheConfig {
834 pub max_entries: usize,
836 pub default_ttl: Duration,
838 pub max_body_size: usize,
840 pub cacheable_status_codes: Vec<u16>,
842 pub cacheable_methods: Vec<String>,
844}
845
846impl Default for ResponseCacheConfig {
847 fn default() -> Self {
848 Self {
849 max_entries: 1000,
850 default_ttl: Duration::from_secs(300), max_body_size: 1024 * 1024, cacheable_status_codes: vec![200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501],
853 cacheable_methods: vec![
854 "GET".to_string(),
855 "HEAD".to_string(),
856 "QUERY".to_string(),
859 ],
860 }
861 }
862}
863
864impl ResponseCacheConfig {
865 pub fn new() -> Self {
867 Self::default()
868 }
869
870 pub fn max_entries(mut self, count: usize) -> Self {
872 self.max_entries = count;
873 self
874 }
875
876 pub fn default_ttl(mut self, ttl: Duration) -> Self {
878 self.default_ttl = ttl;
879 self
880 }
881
882 pub fn max_body_size(mut self, size: usize) -> Self {
884 self.max_body_size = size;
885 self
886 }
887}
888
889impl ResponseCache {
890 pub fn new() -> Self {
892 Self::with_config(ResponseCacheConfig::default())
893 }
894
895 pub fn with_config(config: ResponseCacheConfig) -> Self {
897 Self {
898 config,
899 entries: Arc::new(RwLock::new(HashMap::new())),
900 vary_index: Arc::new(RwLock::new(HashMap::new())),
901 eviction: Mutex::new(EvictionIndex::default()),
902 }
903 }
904
905 pub async fn get(&self, request: &HttpRequest) -> Option<HttpResponse> {
911 let base_key = CacheKey::from_request(request).to_string_key();
912 let vary_headers = {
913 let vary_index = self.vary_index.read().await;
914 vary_index.get(&base_key).cloned().unwrap_or_default()
915 };
916 let vary_refs: Vec<&str> = vary_headers.iter().map(String::as_str).collect();
917 self.get_with_vary(request, &vary_refs).await
918 }
919
920 pub async fn get_with_vary(
922 &self,
923 request: &HttpRequest,
924 vary_headers: &[&str],
925 ) -> Option<HttpResponse> {
926 let key = CacheKey::from_request_with_vary(request, vary_headers);
927 let key_str = key.to_string_key();
928
929 let entries = self.entries.read().await;
930 if let Some(cached) = entries.get(&key_str)
931 && cached.is_fresh()
932 {
933 return Some(cached.to_response());
934 }
935 None
936 }
937
938 pub async fn store(&self, request: &HttpRequest, response: &HttpResponse) {
944 let ttl = response
945 .headers
946 .get("Cache-Control")
947 .map(|h| CacheControl::parse(h))
948 .and_then(|cc| cc.freshness_lifetime())
949 .map(Duration::from_secs)
950 .unwrap_or(self.config.default_ttl);
951
952 self.store_with_ttl(request, response, ttl).await
953 }
954
955 pub async fn store_with_ttl(
957 &self,
958 request: &HttpRequest,
959 response: &HttpResponse,
960 ttl: Duration,
961 ) {
962 if !self.is_cacheable(request, response) {
964 return;
965 }
966
967 let vary_headers: Vec<&str> = response
969 .headers
970 .get("Vary")
971 .map(|v| v.split(',').map(|s| s.trim()).collect())
972 .unwrap_or_default();
973
974 let key = CacheKey::from_request_with_vary(request, &vary_headers);
975 let key_str = key.to_string_key();
976 let base_key = CacheKey::from_request(request).to_string_key();
977 let mut cached = CachedResponse::new(response, ttl);
978 cached.base_key = Some(base_key.clone());
979
980 let mut evicted_base_key = None;
983 {
984 let mut entries = self.entries.write().await;
985
986 if entries.len() >= self.config.max_entries {
988 evicted_base_key = self.evict_oldest(&mut entries);
989 }
990
991 let mut index = self.eviction.lock().unwrap();
998 let seq = index.next_seq();
999 cached.eviction_seq = seq;
1000 let replaced = entries.insert(key_str.clone(), cached).is_some();
1001 index.record_insert(&key_str, seq, &base_key, replaced);
1002 index.compact_if_needed(&entries);
1003 }
1004
1005 let mut vary_index = self.vary_index.write().await;
1007
1008 if let Some(evicted) = evicted_base_key
1011 && evicted != base_key
1012 {
1013 vary_index.remove(&evicted);
1014 }
1015
1016 let entry = vary_index.entry(base_key).or_default();
1021 for header in vary_headers.iter().map(|s| s.to_string()) {
1022 if !entry.contains(&header) {
1023 entry.push(header);
1024 }
1025 }
1026 }
1027
1028 fn is_cacheable(&self, request: &HttpRequest, response: &HttpResponse) -> bool {
1030 if !self
1032 .config
1033 .cacheable_methods
1034 .iter()
1035 .any(|m| m == request.method_str())
1036 {
1037 return false;
1038 }
1039
1040 if !self
1042 .config
1043 .cacheable_status_codes
1044 .contains(&response.status)
1045 {
1046 return false;
1047 }
1048
1049 if response.body.len() > self.config.max_body_size {
1051 return false;
1052 }
1053
1054 let cache_control = response
1057 .headers
1058 .get("Cache-Control")
1059 .map(|h| CacheControl::parse(h));
1060 if let Some(ref cc) = cache_control
1061 && (cc.is_no_store() || cc.is_private() || cc.is_no_cache())
1062 {
1063 return false;
1064 }
1065
1066 let has_authorization = request
1070 .headers
1071 .get("Authorization")
1072 .or_else(|| request.headers.get("authorization"))
1073 .is_some();
1074 if has_authorization {
1075 let explicitly_allowed = cache_control.as_ref().is_some_and(|cc| {
1076 cc.is_public() || cc.get_s_maxage().is_some() || cc.is_must_revalidate()
1077 });
1078 if !explicitly_allowed {
1079 return false;
1080 }
1081 }
1082
1083 true
1084 }
1085
1086 fn evict_oldest(&self, entries: &mut HashMap<String, CachedResponse>) -> Option<String> {
1094 let mut index = self.eviction.lock().unwrap();
1095
1096 while let Some((oldest_key, seq)) = index.order.pop_front() {
1107 let is_current = entries
1108 .get(&oldest_key)
1109 .is_some_and(|e| e.eviction_seq == seq);
1110 if is_current {
1111 let removed = entries.remove(&oldest_key);
1112 return match removed.and_then(|r| r.base_key) {
1116 Some(base_key) => index.record_remove(&base_key).then_some(base_key),
1117 None => None,
1118 };
1119 }
1120 index.dead = index.dead.saturating_sub(1);
1123 }
1124 None
1125 }
1126
1127 pub async fn invalidate(&self, request: &HttpRequest) {
1129 let base_key = CacheKey::from_request(request).to_string_key();
1130 let variant_prefix = format!("{}|", base_key);
1133
1134 {
1135 let mut entries = self.entries.write().await;
1136 let mut removed_count = 0usize;
1137 entries.retain(|key, _| {
1138 if key == &base_key || key.starts_with(&variant_prefix) {
1139 removed_count += 1;
1140 false
1141 } else {
1142 true
1143 }
1144 });
1145 let mut index = self.eviction.lock().unwrap();
1146 index.base_key_counts.remove(&base_key);
1148 index.dead += removed_count;
1151 index.compact_if_needed(&entries);
1152 }
1153
1154 let mut vary_index = self.vary_index.write().await;
1155 vary_index.remove(&base_key);
1156 }
1157
1158 pub async fn invalidate_prefix(&self, path_prefix: &str) {
1160 let needle = format!(":{}", path_prefix);
1161 let mut entries = self.entries.write().await;
1162 let mut index = self.eviction.lock().unwrap();
1163 entries.retain(|key, v| {
1164 if key.contains(&needle) {
1165 if let Some(bk) = &v.base_key {
1166 index.record_remove(bk);
1167 }
1168 index.dead += 1;
1171 false
1172 } else {
1173 true
1174 }
1175 });
1176 index.compact_if_needed(&entries);
1177 }
1178
1179 pub async fn clear(&self) {
1181 {
1182 let mut entries = self.entries.write().await;
1183 entries.clear();
1184 self.eviction.lock().unwrap().clear();
1185 }
1186 let mut vary_index = self.vary_index.write().await;
1187 vary_index.clear();
1188 }
1189
1190 pub async fn purge_stale(&self) {
1196 let dead_base_keys = {
1197 let mut entries = self.entries.write().await;
1198 let mut index = self.eviction.lock().unwrap();
1199
1200 let mut removed_base_keys: Vec<String> = Vec::new();
1203 entries.retain(|_, v| {
1204 if v.is_fresh() {
1205 true
1206 } else {
1207 if let Some(bk) = &v.base_key {
1208 removed_base_keys.push(bk.clone());
1209 index.record_remove(bk);
1210 }
1211 index.dead += 1;
1214 false
1215 }
1216 });
1217
1218 removed_base_keys.retain(|bk| !index.base_key_live(bk));
1222 index.compact_if_needed(&entries);
1223 removed_base_keys
1224 };
1225
1226 if !dead_base_keys.is_empty() {
1227 let mut vary_index = self.vary_index.write().await;
1228 for bk in dead_base_keys {
1229 vary_index.remove(&bk);
1230 }
1231 }
1232 }
1233
1234 pub async fn stats(&self) -> CacheStats {
1236 let entries = self.entries.read().await;
1237 let fresh_count = entries.values().filter(|e| e.is_fresh()).count();
1238 let stale_count = entries.len() - fresh_count;
1239 let total_size: usize = entries.values().map(|e| e.response.body.len()).sum();
1240
1241 CacheStats {
1242 total_entries: entries.len(),
1243 fresh_entries: fresh_count,
1244 stale_entries: stale_count,
1245 total_size_bytes: total_size,
1246 max_entries: self.config.max_entries,
1247 }
1248 }
1249}
1250
1251impl Default for ResponseCache {
1252 fn default() -> Self {
1253 Self::new()
1254 }
1255}
1256
1257#[derive(Debug, Clone)]
1259pub struct CacheStats {
1260 pub total_entries: usize,
1262 pub fresh_entries: usize,
1264 pub stale_entries: usize,
1266 pub total_size_bytes: usize,
1268 pub max_entries: usize,
1270}
1271
1272impl HttpRequest {
1278 pub fn cache_control(&self) -> Option<CacheControl> {
1280 self.headers.get("Cache-Control").map(CacheControl::parse)
1282 }
1283
1284 pub fn allows_cached(&self) -> bool {
1286 if let Some(cc) = self.cache_control() {
1287 !cc.is_no_cache() && !cc.is_no_store()
1289 } else {
1290 true
1291 }
1292 }
1293
1294 pub fn max_stale(&self) -> Option<u64> {
1296 self.cache_control().and_then(|cc| {
1297 cc.directives.iter().find_map(|d| match d {
1298 CacheDirective::MaxStale(secs) => Some(secs.unwrap_or(u64::MAX)),
1299 _ => None,
1300 })
1301 })
1302 }
1303
1304 pub fn cache_key(&self) -> CacheKey {
1306 CacheKey::from_request(self)
1307 }
1308
1309 pub fn cache_key_with_vary(&self, vary_headers: &[&str]) -> CacheKey {
1311 CacheKey::from_request_with_vary(self, vary_headers)
1312 }
1313}
1314
1315impl HttpResponse {
1317 pub fn with_cache_control(mut self, cache_control: CacheControl) -> Self {
1319 self.headers
1320 .insert("Cache-Control".to_string(), cache_control.to_header_value());
1321 self
1322 }
1323
1324 pub fn cache_public(self, max_age: Duration) -> Self {
1326 self.with_cache_control(CacheControl::public_max_age(max_age))
1327 }
1328
1329 pub fn cache_private(self, max_age: Duration) -> Self {
1331 self.with_cache_control(CacheControl::private_max_age(max_age))
1332 }
1333
1334 pub fn cache_immutable(self, max_age: Duration) -> Self {
1336 self.with_cache_control(CacheControl::immutable_asset(max_age))
1337 }
1338
1339 pub fn with_vary(mut self, headers: &[&str]) -> Self {
1341 let vary = headers.join(", ");
1342 self.headers.insert("Vary".to_string(), vary);
1343 self
1344 }
1345
1346 pub fn get_cache_control(&self) -> Option<CacheControl> {
1348 self.headers
1349 .get("Cache-Control")
1350 .map(|h| CacheControl::parse(h))
1351 }
1352
1353 pub fn is_cacheable(&self) -> bool {
1355 if let Some(cc) = self.get_cache_control() {
1356 cc.is_cacheable()
1357 } else {
1358 self.status == 200
1360 }
1361 }
1362}
1363
1364#[cfg(test)]
1369mod tests {
1370 use super::*;
1371 use bytes::Bytes;
1372
1373 #[test]
1374 fn test_cache_directive_parse() {
1375 assert_eq!(
1376 CacheDirective::parse("public"),
1377 Some(CacheDirective::Public)
1378 );
1379 assert_eq!(
1380 CacheDirective::parse("private"),
1381 Some(CacheDirective::Private)
1382 );
1383 assert_eq!(
1384 CacheDirective::parse("no-store"),
1385 Some(CacheDirective::NoStore)
1386 );
1387 assert_eq!(
1388 CacheDirective::parse("max-age=3600"),
1389 Some(CacheDirective::MaxAge(3600))
1390 );
1391 }
1392
1393 #[test]
1394 fn test_cache_control_parse() {
1395 let cc = CacheControl::parse("public, max-age=3600, must-revalidate");
1396 assert!(cc.is_public());
1397 assert_eq!(cc.get_max_age(), Some(3600));
1398 assert!(cc.is_must_revalidate());
1399 }
1400
1401 #[test]
1402 fn test_cache_control_builder() {
1403 let cc = CacheControl::new()
1404 .public()
1405 .max_age(Duration::from_secs(3600))
1406 .must_revalidate();
1407
1408 assert_eq!(
1409 cc.to_header_value(),
1410 "public, max-age=3600, must-revalidate"
1411 );
1412 }
1413
1414 #[test]
1415 fn test_cache_control_presets() {
1416 let never = CacheControl::never();
1417 assert!(never.is_no_store());
1418 assert!(never.is_no_cache());
1419
1420 let public = CacheControl::public_max_age(Duration::from_secs(3600));
1421 assert!(public.is_public());
1422 assert_eq!(public.get_max_age(), Some(3600));
1423
1424 let immutable = CacheControl::immutable_asset(Duration::from_secs(31536000));
1425 assert!(immutable.is_immutable());
1426 }
1427
1428 #[test]
1429 fn test_cache_control_is_cacheable() {
1430 assert!(CacheControl::public_max_age(Duration::from_secs(3600)).is_cacheable());
1431 assert!(CacheControl::private_max_age(Duration::from_secs(3600)).is_cacheable());
1432 assert!(!CacheControl::never().is_cacheable());
1433 }
1434
1435 #[test]
1436 fn test_cache_key_from_request() {
1437 let request = HttpRequest::new("GET", "/api/users?page=1&limit=10");
1438
1439 let key = CacheKey::from_request(&request);
1440 assert_eq!(key.method, "GET");
1441 assert_eq!(key.path, "/api/users");
1442 assert_eq!(key.query, "5:limit=2:10&4:page=1:1&");
1444 }
1445
1446 #[test]
1447 fn cache_key_query_does_not_collide_across_decoded_delimiters() {
1448 let one = HttpRequest::new("GET", "/search?a=1%26b%3D2");
1450 let two = HttpRequest::new("GET", "/search?a=1&b=2");
1452
1453 assert_ne!(
1454 CacheKey::from_request(&one),
1455 CacheKey::from_request(&two),
1456 "distinct requests must not share a cache entry"
1457 );
1458 }
1459
1460 #[test]
1461 fn test_cache_key_with_vary() {
1462 let mut request = HttpRequest::new("GET", "/api/users".to_string());
1463 request
1464 .headers
1465 .insert("Accept", "application/json".to_string());
1466
1467 let key = CacheKey::from_request_with_vary(&request, &["Accept"]);
1468 assert_eq!(key.vary_values.len(), 1);
1469 assert_eq!(
1470 key.vary_values[0],
1471 ("accept".to_string(), "application/json".to_string())
1472 );
1473 }
1474
1475 #[test]
1476 fn test_cached_response() {
1477 let mut response = HttpResponse::ok();
1478 response.body = Bytes::from_static(b"Hello, World!");
1479 response
1480 .headers
1481 .insert("ETag".to_string(), "\"abc123\"".to_string());
1482
1483 let cached = CachedResponse::new(&response, Duration::from_secs(300));
1484 assert!(cached.is_fresh());
1485 assert_eq!(cached.etag, Some("\"abc123\"".to_string()));
1486 }
1487
1488 #[tokio::test]
1489 async fn test_response_cache_store_and_get() {
1490 let cache = ResponseCache::new();
1491 let request = HttpRequest::new("GET", "/api/users".to_string());
1492 let mut response = HttpResponse::ok();
1493 response.body = Bytes::from_static(b"cached content");
1494
1495 cache.store(&request, &response).await;
1496
1497 let cached = cache.get(&request).await;
1498 assert!(cached.is_some());
1499 assert_eq!(cached.unwrap().body, Bytes::from_static(b"cached content"));
1500 }
1501
1502 #[tokio::test]
1503 async fn test_query_method_cached_with_body_in_key() {
1504 let cache = ResponseCache::new();
1505
1506 let mut search_a = HttpRequest::new("QUERY", "/search".to_string());
1507 search_a.body = Bytes::from_static(b"name=alice");
1508 let mut response_a = HttpResponse::ok();
1509 response_a.body = Bytes::from_static(b"results for alice");
1510
1511 cache.store(&search_a, &response_a).await;
1512
1513 let cached = cache.get(&search_a).await;
1515 assert!(cached.is_some());
1516 assert_eq!(
1517 cached.unwrap().body,
1518 Bytes::from_static(b"results for alice")
1519 );
1520
1521 let mut search_b = HttpRequest::new("QUERY", "/search".to_string());
1523 search_b.body = Bytes::from_static(b"name=bob");
1524 assert!(cache.get(&search_b).await.is_none());
1525
1526 let mut response_b = HttpResponse::ok();
1528 response_b.body = Bytes::from_static(b"results for bob");
1529 cache.store(&search_b, &response_b).await;
1530 assert_eq!(
1531 cache.get(&search_a).await.unwrap().body,
1532 Bytes::from_static(b"results for alice")
1533 );
1534 assert_eq!(
1535 cache.get(&search_b).await.unwrap().body,
1536 Bytes::from_static(b"results for bob")
1537 );
1538 }
1539
1540 #[test]
1541 fn test_query_cache_key_includes_body_hash() {
1542 let mut req_a = HttpRequest::new("QUERY", "/search".to_string());
1543 req_a.body = Bytes::from_static(b"a");
1544 let mut req_b = HttpRequest::new("QUERY", "/search".to_string());
1545 req_b.body = Bytes::from_static(b"b");
1546
1547 let key_a = CacheKey::from_request(&req_a);
1548 let key_b = CacheKey::from_request(&req_b);
1549 assert!(key_a.body_hash.is_some());
1550 assert_ne!(key_a, key_b);
1551 assert_ne!(key_a.to_string_key(), key_b.to_string_key());
1552
1553 let mut get_req = HttpRequest::new("GET", "/search".to_string());
1555 get_req.body = Bytes::from_static(b"ignored");
1556 assert!(CacheKey::from_request(&get_req).body_hash.is_none());
1557 }
1558
1559 #[tokio::test]
1560 async fn test_response_cache_invalidate() {
1561 let cache = ResponseCache::new();
1562 let request = HttpRequest::new("GET", "/api/users".to_string());
1563 let response = HttpResponse::ok();
1564
1565 cache.store(&request, &response).await;
1566 assert!(cache.get(&request).await.is_some());
1567
1568 cache.invalidate(&request).await;
1569 assert!(cache.get(&request).await.is_none());
1570 }
1571
1572 #[tokio::test]
1573 async fn test_response_cache_respects_no_store() {
1574 let cache = ResponseCache::new();
1575 let request = HttpRequest::new("GET", "/api/users".to_string());
1576 let response = HttpResponse::ok().no_cache();
1577
1578 cache.store(&request, &response).await;
1579
1580 assert!(cache.get(&request).await.is_none());
1582 }
1583
1584 #[tokio::test]
1585 async fn test_response_cache_respects_private() {
1586 let cache = ResponseCache::new();
1587 let request = HttpRequest::new("GET", "/api/users".to_string());
1588 let response = HttpResponse::ok().cache_private(Duration::from_secs(300));
1589
1590 cache.store(&request, &response).await;
1591
1592 assert!(cache.get(&request).await.is_none());
1594 }
1595
1596 #[tokio::test]
1597 async fn test_response_cache_respects_no_cache_directive() {
1598 let cache = ResponseCache::new();
1599 let request = HttpRequest::new("GET", "/api/users".to_string());
1600 let response = HttpResponse::ok().with_cache_control(CacheControl::new().no_cache());
1601
1602 cache.store(&request, &response).await;
1603
1604 assert!(cache.get(&request).await.is_none());
1605 }
1606
1607 #[tokio::test]
1608 async fn test_response_cache_authorization_not_stored() {
1609 let cache = ResponseCache::new();
1610 let mut request = HttpRequest::new("GET", "/api/me".to_string());
1611 request
1612 .headers
1613 .insert("Authorization", "Bearer user-a".to_string());
1614 let response = HttpResponse::ok();
1615
1616 cache.store(&request, &response).await;
1617
1618 assert!(cache.get(&request).await.is_none());
1621 }
1622
1623 #[tokio::test]
1624 async fn test_response_cache_authorization_stored_when_public() {
1625 let cache = ResponseCache::new();
1626 let mut request = HttpRequest::new("GET", "/api/assets".to_string());
1627 request
1628 .headers
1629 .insert("Authorization", "Bearer user-a".to_string());
1630 let response = HttpResponse::ok().cache_public(Duration::from_secs(60));
1631
1632 cache.store(&request, &response).await;
1633
1634 assert!(cache.get(&request).await.is_some());
1635 }
1636
1637 #[tokio::test]
1638 async fn test_response_cache_ttl_from_max_age() {
1639 let cache = ResponseCache::new();
1640 let request = HttpRequest::new("GET", "/api/users".to_string());
1641 let response = HttpResponse::ok().cache_public(Duration::from_secs(0));
1643
1644 cache.store(&request, &response).await;
1645
1646 assert!(cache.get(&request).await.is_none());
1647 }
1648
1649 #[tokio::test]
1650 async fn test_response_cache_vary_two_phase_lookup() {
1651 let cache = ResponseCache::new();
1652 let mut request = HttpRequest::new("GET", "/api/data".to_string());
1653 request
1654 .headers
1655 .insert("Accept", "application/json".to_string());
1656
1657 let mut response = HttpResponse::ok().with_vary(&["Accept"]);
1658 response.body = Bytes::from_static(b"json");
1659
1660 cache.store(&request, &response).await;
1661
1662 let hit = cache.get(&request).await;
1664 assert!(hit.is_some());
1665 assert_eq!(hit.unwrap().body, Bytes::from_static(b"json"));
1666
1667 let mut other = HttpRequest::new("GET", "/api/data".to_string());
1669 other.headers.insert("Accept", "text/xml".to_string());
1670 assert!(cache.get(&other).await.is_none());
1671 }
1672
1673 #[tokio::test]
1674 async fn test_response_cache_invalidate_removes_vary_variants() {
1675 let cache = ResponseCache::new();
1676 let mut request = HttpRequest::new("GET", "/api/data".to_string());
1677 request
1678 .headers
1679 .insert("Accept", "application/json".to_string());
1680
1681 let response = HttpResponse::ok().with_vary(&["Accept"]);
1682 cache.store(&request, &response).await;
1683 assert!(cache.get(&request).await.is_some());
1684
1685 let plain = HttpRequest::new("GET", "/api/data".to_string());
1687 cache.invalidate(&plain).await;
1688 assert!(cache.get(&request).await.is_none());
1689 }
1690
1691 #[test]
1692 fn test_response_cache_control_methods() {
1693 let response = HttpResponse::ok().cache_public(Duration::from_secs(3600));
1694
1695 let cc = response.get_cache_control().unwrap();
1696 assert!(cc.is_public());
1697 assert_eq!(cc.get_max_age(), Some(3600));
1698 }
1699
1700 #[test]
1701 fn test_response_with_vary() {
1702 let response = HttpResponse::ok().with_vary(&["Accept", "Accept-Encoding"]);
1703
1704 assert_eq!(
1705 response.headers.get("Vary"),
1706 Some(&"Accept, Accept-Encoding".to_string())
1707 );
1708 }
1709
1710 #[test]
1711 fn test_request_allows_cached() {
1712 let request = HttpRequest::new("GET", "/api/users".to_string());
1713 assert!(request.allows_cached());
1714
1715 let mut request_no_cache = HttpRequest::new("GET", "/api/users".to_string());
1716 request_no_cache
1717 .headers
1718 .insert("Cache-Control", "no-cache".to_string());
1719 assert!(!request_no_cache.allows_cached());
1720 }
1721
1722 #[tokio::test]
1728 async fn test_vary_index_bounded_after_eviction() {
1729 let cache = ResponseCache::with_config(ResponseCacheConfig::new().max_entries(8));
1730
1731 for i in 0..100 {
1732 let mut req = HttpRequest::new("QUERY", "/search".to_string());
1733 req.body = Bytes::from(format!("q={}", i).into_bytes());
1734 let mut resp = HttpResponse::ok();
1735 resp.body = Bytes::from(format!("result {}", i).into_bytes());
1736 cache.store(&req, &resp).await;
1737 }
1738
1739 let entries_len = cache.entries.read().await.len();
1740 let vary_len = cache.vary_index.read().await.len();
1741
1742 assert!(
1743 entries_len <= 8,
1744 "entries ({}) exceeded max_entries",
1745 entries_len
1746 );
1747 assert!(
1748 vary_len <= entries_len,
1749 "vary_index ({}) must not exceed entries ({}) after eviction",
1750 vary_len,
1751 entries_len,
1752 );
1753 }
1754
1755 #[tokio::test]
1759 async fn test_evicts_in_insertion_order() {
1760 let cache = ResponseCache::with_config(ResponseCacheConfig::new().max_entries(3));
1761
1762 for i in 0..3 {
1763 let req = HttpRequest::new("GET", format!("/p{}", i));
1764 cache.store(&req, &HttpResponse::ok()).await;
1765 }
1766 for i in 0..3 {
1767 let req = HttpRequest::new("GET", format!("/p{}", i));
1768 assert!(cache.get(&req).await.is_some(), "/p{} should be cached", i);
1769 }
1770
1771 let req = HttpRequest::new("GET", "/p3".to_string());
1773 cache.store(&req, &HttpResponse::ok()).await;
1774
1775 let p0 = HttpRequest::new("GET", "/p0".to_string());
1776 assert!(
1777 cache.get(&p0).await.is_none(),
1778 "oldest entry (/p0) must be evicted first"
1779 );
1780 for i in 1..4 {
1781 let req = HttpRequest::new("GET", format!("/p{}", i));
1782 assert!(cache.get(&req).await.is_some(), "/p{} must remain", i);
1783 }
1784
1785 let entries_len = cache.entries.read().await.len();
1787 let vary_len = cache.vary_index.read().await.len();
1788 assert!(entries_len <= 3);
1789 assert!(vary_len <= entries_len);
1790 }
1791
1792 #[tokio::test]
1800 async fn test_eviction_refresh_resets_recency() {
1801 async fn store(cache: &ResponseCache, path: &str, body: &[u8]) {
1802 let req = HttpRequest::new("GET", path.to_string());
1803 let mut resp = HttpResponse::ok();
1804 resp.body = Bytes::copy_from_slice(body);
1805 cache.store(&req, &resp).await;
1806 }
1807 async fn present(cache: &ResponseCache, path: &str) -> bool {
1808 cache
1809 .get(&HttpRequest::new("GET", path.to_string()))
1810 .await
1811 .is_some()
1812 }
1813
1814 let cache = ResponseCache::with_config(ResponseCacheConfig::new().max_entries(3));
1815
1816 store(&cache, "/a", b"a").await;
1817 store(&cache, "/b", b"b").await;
1818 store(&cache, "/a", b"a2").await; store(&cache, "/c", b"c").await;
1820 store(&cache, "/d", b"d").await; assert!(
1823 !present(&cache, "/b").await,
1824 "B (the oldest untouched entry) must be evicted"
1825 );
1826 assert!(
1827 present(&cache, "/a").await,
1828 "A was refreshed under capacity and must survive"
1829 );
1830 assert!(present(&cache, "/c").await, "/c must remain");
1831 assert!(present(&cache, "/d").await, "/d was just stored");
1832 }
1833
1834 #[tokio::test]
1837 async fn test_purge_stale_shrinks_vary_index() {
1838 let cache = ResponseCache::new();
1839
1840 let mut stale_req = HttpRequest::new("QUERY", "/search".to_string());
1841 stale_req.body = Bytes::from_static(b"q=stale");
1842 let mut fresh_req = HttpRequest::new("QUERY", "/search".to_string());
1843 fresh_req.body = Bytes::from_static(b"q=fresh");
1844 let resp = HttpResponse::ok();
1845
1846 cache
1847 .store_with_ttl(&stale_req, &resp, Duration::from_secs(0))
1848 .await;
1849 cache
1850 .store_with_ttl(&fresh_req, &resp, Duration::from_secs(300))
1851 .await;
1852
1853 assert_eq!(cache.vary_index.read().await.len(), 2);
1854
1855 tokio::time::sleep(Duration::from_millis(5)).await;
1857 cache.purge_stale().await;
1858
1859 assert_eq!(
1860 cache.entries.read().await.len(),
1861 1,
1862 "only the fresh entry should survive purge",
1863 );
1864 assert_eq!(
1865 cache.vary_index.read().await.len(),
1866 1,
1867 "vary_index must shrink in lockstep with purged entries",
1868 );
1869 }
1870
1871 #[tokio::test]
1876 async fn test_eviction_order_bounded_under_store_invalidate_churn() {
1877 let cache = ResponseCache::with_config(ResponseCacheConfig::new().max_entries(10_000));
1878
1879 for i in 0..2000 {
1880 let req = HttpRequest::new("GET", format!("/churn/{}", i % 5));
1881 cache.store(&req, &HttpResponse::ok()).await;
1882 cache.invalidate(&req).await;
1883 }
1884
1885 let live = cache.entries.read().await.len();
1886 let order_len = cache.eviction.lock().unwrap().order.len();
1887 assert!(
1888 order_len <= 2 * live + 16,
1889 "order ({}) grew unbounded relative to live entries ({}) under store/invalidate churn",
1890 order_len,
1891 live
1892 );
1893 }
1894
1895 #[tokio::test]
1901 async fn test_eviction_order_bounded_under_repeated_restore() {
1902 let cache = ResponseCache::with_config(ResponseCacheConfig::new().max_entries(10_000));
1903 let req = HttpRequest::new("GET", "/hot".to_string());
1904
1905 for i in 0..2000 {
1906 let mut resp = HttpResponse::ok();
1907 resp.body = Bytes::from(format!("v{}", i).into_bytes());
1908 cache.store(&req, &resp).await;
1909 }
1910
1911 let live = cache.entries.read().await.len();
1912 assert_eq!(live, 1, "only the latest write for the key should be live");
1913
1914 let order_len = cache.eviction.lock().unwrap().order.len();
1915 assert!(
1916 order_len <= 2 * live + 16,
1917 "order ({}) grew unbounded across repeated re-stores of one key (live={})",
1918 order_len,
1919 live
1920 );
1921
1922 let cached = cache.get(&req).await;
1924 assert!(cached.is_some());
1925 assert_eq!(cached.unwrap().body, Bytes::from_static(b"v1999"));
1926 }
1927
1928 #[tokio::test]
1933 async fn test_vary_index_merges_distinct_vary_sets() {
1934 let cache = ResponseCache::new();
1935
1936 let mut req_accept = HttpRequest::new("GET", "/api/data".to_string());
1938 req_accept
1939 .headers
1940 .insert("Accept", "application/json".to_string());
1941 let mut resp_accept = HttpResponse::ok().with_vary(&["Accept"]);
1942 resp_accept.body = Bytes::from_static(b"json-body");
1943 cache.store(&req_accept, &resp_accept).await;
1944
1945 let mut req_enc = HttpRequest::new("GET", "/api/data".to_string());
1947 req_enc
1948 .headers
1949 .insert("Accept-Encoding", "gzip".to_string());
1950 let mut resp_enc = HttpResponse::ok().with_vary(&["Accept-Encoding"]);
1951 resp_enc.body = Bytes::from_static(b"gzip-body");
1952 cache.store(&req_enc, &resp_enc).await;
1953
1954 let hit_accept = cache.get(&req_accept).await;
1956 assert!(
1957 hit_accept.is_some(),
1958 "Accept variant lost after second store"
1959 );
1960 assert_eq!(hit_accept.unwrap().body, Bytes::from_static(b"json-body"));
1961
1962 let hit_enc = cache.get(&req_enc).await;
1963 assert!(
1964 hit_enc.is_some(),
1965 "Accept-Encoding variant lost after second store",
1966 );
1967 assert_eq!(hit_enc.unwrap().body, Bytes::from_static(b"gzip-body"));
1968 }
1969}