1use std::error::Error as StdError;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::task::{Context, Poll};
6use std::time::{Duration, SystemTime};
7
8use bytes::Bytes;
9use dashmap::DashMap;
10use dashmap::mapref::entry::Entry;
11use http::header::{CACHE_CONTROL, PRAGMA};
12use http::{HeaderMap, Method, Request, Response, Uri};
13use http_body::Body;
14use http_body_util::combinators::BoxBody;
15use http_body_util::{BodyExt, Full};
16use tokio::sync::{Mutex, OwnedMutexGuard};
17use tower::{Layer, Service, ServiceExt};
18
19#[cfg(feature = "metrics")]
20use metrics::{counter, histogram};
21
22#[cfg(feature = "in-memory")]
23use crate::backend::memory::InMemoryBackend;
24use crate::backend::{CacheBackend, CacheEntry, CacheRead};
25use crate::chunks::{ChunkCache, ChunkMetadata};
26#[cfg(feature = "compression")]
27use crate::policy::CompressionStrategy;
28use crate::policy::{CachePolicy, CompressionConfig};
29use crate::range::{RangeHandling, is_partial_content, parse_range_header};
30use crate::refresh::{AutoRefreshConfig, RefreshCallback, RefreshManager, RefreshMetadata};
31#[cfg(feature = "tracing")]
32use crate::streaming::extract_size_info;
33use crate::streaming::{StreamingDecision, should_stream};
34
35pub type BoxError = Box<dyn StdError + Send + Sync>;
36
37type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
43
44pin_project_lite::pin_project! {
45 pub struct SyncBoxBody {
50 #[pin]
51 inner: BoxBody<Bytes, BoxError>,
52 }
53}
54
55impl SyncBoxBody {
56 pub fn new(inner: BoxBody<Bytes, BoxError>) -> Self {
58 Self { inner }
59 }
60}
61
62unsafe impl Sync for SyncBoxBody {}
66
67impl Body for SyncBoxBody {
68 type Data = Bytes;
69 type Error = BoxError;
70
71 fn poll_frame(
72 self: std::pin::Pin<&mut Self>,
73 cx: &mut std::task::Context<'_>,
74 ) -> std::task::Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
75 self.project().inner.poll_frame(cx)
76 }
77
78 fn is_end_stream(&self) -> bool {
79 self.inner.is_end_stream()
80 }
81
82 fn size_hint(&self) -> http_body::SizeHint {
83 self.inner.size_hint()
84 }
85}
86
87type KeyExtractorFn = Arc<dyn Fn(&Method, &Uri) -> Option<String> + Send + Sync>;
89
90#[derive(Clone)]
99pub struct CacheLayer<B> {
100 backend: B,
101 policy: CachePolicy,
102 key_extractor: KeyExtractor,
103 locks: Arc<DashMap<String, Arc<Mutex<()>>>>,
104 refresh_manager: Option<Arc<RefreshManager>>,
105 chunk_cache: Option<Arc<ChunkCache>>,
106}
107
108#[derive(Clone)]
114pub struct KeyExtractor {
115 inner: KeyExtractorFn,
116}
117
118impl KeyExtractor {
119 pub fn path_and_query() -> Self {
121 Self {
122 inner: Arc::new(|method: &Method, uri: &Uri| {
123 if matches!(method, &Method::GET | &Method::HEAD) {
124 let mut key = uri.path().to_owned();
125 if let Some(query) = uri.query() {
126 key.push('?');
127 key.push_str(query);
128 }
129 Some(key)
130 } else {
131 None
132 }
133 }),
134 }
135 }
136
137 pub fn path() -> Self {
138 Self {
139 inner: Arc::new(|method: &Method, uri: &Uri| {
140 if matches!(method, &Method::GET | &Method::HEAD) {
141 Some(uri.path().to_owned())
142 } else {
143 None
144 }
145 }),
146 }
147 }
148
149 pub fn custom<F>(func: F) -> Self
150 where
151 F: Fn(&Method, &Uri) -> Option<String> + Send + Sync + 'static,
152 {
153 Self {
154 inner: Arc::new(func),
155 }
156 }
157
158 pub fn extract(&self, method: &Method, uri: &Uri) -> Option<String> {
162 (self.inner)(method, uri)
163 }
164}
165
166impl Default for KeyExtractor {
167 fn default() -> Self {
168 Self::path_and_query()
169 }
170}
171
172pub struct CacheLayerBuilder<B> {
174 backend: B,
175 policy: CachePolicy,
176 key_extractor: KeyExtractor,
177 auto_refresh_config: Option<AutoRefreshConfig>,
178}
179
180impl<B> CacheLayerBuilder<B>
181where
182 B: CacheBackend,
183{
184 pub fn new(backend: B) -> Self {
185 Self {
186 backend,
187 policy: CachePolicy::default(),
188 key_extractor: KeyExtractor::default(),
189 auto_refresh_config: None,
190 }
191 }
192
193 pub fn policy(mut self, policy: CachePolicy) -> Self {
195 self.policy = policy;
196 self
197 }
198
199 pub fn ttl(mut self, ttl: Duration) -> Self {
201 self.policy = self.policy.with_ttl(ttl);
202 self
203 }
204
205 pub fn negative_ttl(mut self, ttl: Duration) -> Self {
207 self.policy = self.policy.with_negative_ttl(ttl);
208 self
209 }
210
211 pub fn stale_while_revalidate(mut self, duration: Duration) -> Self {
212 self.policy = self.policy.with_stale_while_revalidate(duration);
213 self
214 }
215
216 pub fn refresh_before(mut self, duration: Duration) -> Self {
217 self.policy = self.policy.with_refresh_before(duration);
218 self
219 }
220
221 pub fn max_body_size(mut self, size: Option<usize>) -> Self {
222 self.policy = self.policy.with_max_body_size(size);
223 self
224 }
225
226 pub fn min_body_size(mut self, size: Option<usize>) -> Self {
227 self.policy = self.policy.with_min_body_size(size);
228 self
229 }
230
231 pub fn allow_streaming_bodies(mut self, allow: bool) -> Self {
232 self.policy = self.policy.with_allow_streaming_bodies(allow);
233 self
234 }
235
236 pub fn compression(mut self, config: CompressionConfig) -> Self {
237 self.policy = self.policy.with_compression(config);
238 self
239 }
240
241 pub fn respect_cache_control(mut self, enabled: bool) -> Self {
242 self.policy = self.policy.with_respect_cache_control(enabled);
243 self
244 }
245
246 pub fn statuses(mut self, statuses: impl IntoIterator<Item = u16>) -> Self {
247 self.policy = self.policy.with_statuses(statuses);
248 self
249 }
250
251 pub fn method_predicate<F>(mut self, predicate: F) -> Self
252 where
253 F: Fn(&Method) -> bool + Send + Sync + 'static,
254 {
255 self.policy = self.policy.with_method_predicate(predicate);
256 self
257 }
258
259 pub fn header_allowlist<I, S>(mut self, headers: I) -> Self
260 where
261 I: IntoIterator<Item = S>,
262 S: Into<String>,
263 {
264 self.policy = self.policy.with_header_allowlist(headers);
265 self
266 }
267
268 pub fn key_extractor(mut self, extractor: KeyExtractor) -> Self {
269 self.key_extractor = extractor;
270 self
271 }
272
273 pub fn auto_refresh(mut self, config: AutoRefreshConfig) -> Self {
278 self.auto_refresh_config = Some(config);
279 self
280 }
281
282 pub fn build(self) -> CacheLayer<B> {
283 let refresh_manager = self
284 .auto_refresh_config
285 .filter(|cfg| cfg.enabled)
286 .map(|cfg| Arc::new(RefreshManager::new(cfg)));
287
288 let chunk_cache = if self.policy.streaming_policy().enable_chunk_cache {
290 Some(Arc::new(ChunkCache::new(
291 self.policy.streaming_policy().chunk_size,
292 )))
293 } else {
294 None
295 };
296
297 CacheLayer {
298 backend: self.backend,
299 policy: self.policy,
300 key_extractor: self.key_extractor,
301 locks: Arc::new(DashMap::new()),
302 refresh_manager,
303 chunk_cache,
304 }
305 }
306}
307
308#[cfg(feature = "in-memory")]
309impl CacheLayer<InMemoryBackend> {
310 pub fn new_in_memory(max_capacity: u64) -> Self {
312 CacheLayerBuilder::new(InMemoryBackend::new(max_capacity)).build()
313 }
314}
315
316impl<B> CacheLayer<B>
317where
318 B: CacheBackend,
319{
320 pub fn new(backend: B) -> Self {
322 CacheLayerBuilder::new(backend).build()
323 }
324
325 pub fn builder(backend: B) -> CacheLayerBuilder<B> {
327 CacheLayerBuilder::new(backend)
328 }
329
330 pub fn with_policy(mut self, policy: CachePolicy) -> Self {
331 self.policy = policy;
332 self
333 }
334
335 pub fn with_ttl(mut self, ttl: Duration) -> Self {
336 self.policy = self.policy.clone().with_ttl(ttl);
337 self
338 }
339
340 pub fn with_negative_ttl(mut self, ttl: Duration) -> Self {
341 self.policy = self.policy.clone().with_negative_ttl(ttl);
342 self
343 }
344
345 pub fn with_stale_while_revalidate(mut self, duration: Duration) -> Self {
346 self.policy = self.policy.clone().with_stale_while_revalidate(duration);
347 self
348 }
349
350 pub fn with_refresh_before(mut self, duration: Duration) -> Self {
351 self.policy = self.policy.clone().with_refresh_before(duration);
352 self
353 }
354
355 pub fn with_max_body_size(mut self, size: Option<usize>) -> Self {
356 self.policy = self.policy.clone().with_max_body_size(size);
357 self
358 }
359
360 pub fn with_min_body_size(mut self, size: Option<usize>) -> Self {
361 self.policy = self.policy.clone().with_min_body_size(size);
362 self
363 }
364
365 pub fn with_allow_streaming_bodies(mut self, allow: bool) -> Self {
366 self.policy = self.policy.clone().with_allow_streaming_bodies(allow);
367 self
368 }
369
370 pub fn with_compression(mut self, config: CompressionConfig) -> Self {
371 self.policy = self.policy.clone().with_compression(config);
372 self
373 }
374
375 pub fn with_respect_cache_control(mut self, enabled: bool) -> Self {
376 self.policy = self.policy.clone().with_respect_cache_control(enabled);
377 self
378 }
379
380 pub fn with_cache_statuses(mut self, statuses: impl IntoIterator<Item = u16>) -> Self {
381 self.policy = self.policy.clone().with_statuses(statuses);
382 self
383 }
384
385 pub fn with_method_predicate<F>(mut self, predicate: F) -> Self
386 where
387 F: Fn(&Method) -> bool + Send + Sync + 'static,
388 {
389 self.policy = self.policy.clone().with_method_predicate(predicate);
390 self
391 }
392
393 pub fn with_header_allowlist<I, S>(mut self, headers: I) -> Self
394 where
395 I: IntoIterator<Item = S>,
396 S: Into<String>,
397 {
398 self.policy = self.policy.clone().with_header_allowlist(headers);
399 self
400 }
401
402 pub fn with_key_extractor(mut self, extractor: KeyExtractor) -> Self {
403 self.key_extractor = extractor;
404 self
405 }
406
407 pub async fn init_auto_refresh<S, ResBody>(&self, service: S) -> Result<(), String>
422 where
423 S: Service<Request<()>, Response = Response<ResBody>> + Clone + Send + Sync + 'static,
424 S::Future: Send + 'static,
425 S::Error: Into<BoxError> + Send,
426 ResBody: Body<Data = Bytes> + Send + 'static,
427 ResBody::Error: Into<BoxError> + Send,
428 B: Clone,
429 {
430 if let Some(ref manager) = self.refresh_manager {
431 let callback = Arc::new(CacheRefreshCallback::new(
432 service,
433 self.backend.clone(),
434 self.policy.clone(),
435 self.key_extractor.clone(),
436 ));
437 manager.start(callback).await
438 } else {
439 Ok(())
440 }
441 }
442}
443
444impl<S, B> Layer<S> for CacheLayer<B>
445where
446 B: CacheBackend,
447{
448 type Service = CacheService<S, B>;
449
450 fn layer(&self, inner: S) -> Self::Service {
451 CacheService {
452 inner,
453 backend: self.backend.clone(),
454 policy: self.policy.clone(),
455 key_extractor: self.key_extractor.clone(),
456 locks: self.locks.clone(),
457 refresh_manager: self.refresh_manager.clone(),
458 chunk_cache: self.chunk_cache.clone(),
459 }
460 }
461}
462
463impl<B> Drop for CacheLayer<B> {
464 fn drop(&mut self) {
465 if let Some(manager) = &self.refresh_manager {
468 let manager = manager.clone();
469 if let Ok(handle) = tokio::runtime::Handle::try_current() {
472 handle.spawn(async move {
473 manager.shutdown().await;
474 });
475 }
476 }
477 }
478}
479
480#[derive(Clone)]
481pub struct CacheService<S, B> {
482 inner: S,
483 backend: B,
484 policy: CachePolicy,
485 key_extractor: KeyExtractor,
486 locks: Arc<DashMap<String, Arc<Mutex<()>>>>,
487 refresh_manager: Option<Arc<RefreshManager>>,
488 chunk_cache: Option<Arc<ChunkCache>>,
489}
490
491struct CacheRefreshCallback<S, B> {
493 inner: S,
494 backend: B,
495 policy: CachePolicy,
496}
497
498impl<S, B> CacheRefreshCallback<S, B> {
499 fn new(inner: S, backend: B, policy: CachePolicy, _key_extractor: KeyExtractor) -> Self {
500 Self {
501 inner,
502 backend,
503 policy,
504 }
505 }
506}
507
508impl<S, B, ResBody> RefreshCallback for CacheRefreshCallback<S, B>
509where
510 S: Service<Request<()>, Response = Response<ResBody>> + Clone + Send + Sync + 'static,
511 S::Future: Send + 'static,
512 S::Error: Into<BoxError> + Send,
513 ResBody: Body<Data = Bytes> + Send + 'static,
514 ResBody::Error: Into<BoxError> + Send,
515 B: CacheBackend,
516{
517 fn refresh(&self, key: String, metadata: RefreshMetadata) -> crate::refresh::RefreshFuture {
518 let backend = self.backend.clone();
519 let policy = self.policy.clone();
520 let inner = self.inner.clone();
521
522 Box::pin(async move {
523 #[cfg(feature = "tracing")]
524 tracing::debug!(key = %key, uri = %metadata.uri, "Auto-refresh triggered");
525
526 let request_method = metadata.method.clone();
529 let request_uri = metadata.uri.clone();
530
531 let request = match metadata.try_into_request() {
533 Some(req) => req,
534 None => {
535 #[cfg(feature = "tracing")]
536 tracing::warn!(key = %key, "Failed to reconstruct request for auto-refresh");
537 return Err("Failed to reconstruct request".into());
538 }
539 };
540
541 let service = inner;
543 let response = match service.oneshot(request).await {
544 Ok(resp) => resp,
545 Err(_err) => {
546 #[cfg(feature = "tracing")]
547 tracing::error!(key = %key, "Service error during auto-refresh");
548 return Err("Service error during refresh".into());
549 }
550 };
551
552 let (parts, body) = response.into_parts();
553
554 let collected = match BodyExt::collect(body).await {
556 Ok(c) => c,
557 Err(_err) => {
558 #[cfg(feature = "tracing")]
559 tracing::error!(key = %key, "Body collection error during auto-refresh");
560 return Err("Body collection error".into());
561 }
562 };
563
564 let cache_bytes = collected.to_bytes();
565
566 let body_too_large = policy
568 .max_body_size()
569 .is_some_and(|max| cache_bytes.len() > max);
570 let body_too_small = policy
571 .min_body_size()
572 .is_some_and(|min| cache_bytes.len() < min);
573
574 if body_too_large || body_too_small {
575 return Ok(()); }
577
578 if let Some(ttl) = policy.ttl_for(parts.status) {
580 if !ttl.is_zero() {
581 let stale_for = policy.stale_while_revalidate();
582 let headers_to_cache = policy.headers_to_cache(&parts.headers);
583 let (compressed_bytes, _compressed) =
584 maybe_compress(cache_bytes, policy.compression());
585
586 let entry = CacheEntry::new(
587 parts.status,
588 parts.version,
589 headers_to_cache,
590 compressed_bytes,
591 );
592 let tags = policy.extract_tags(&request_method, &request_uri);
593 let entry = if tags.is_empty() {
594 entry
595 } else {
596 entry.with_tags(tags)
597 };
598
599 if let Err(_err) = backend.set(key.clone(), entry, ttl, stale_for).await {
600 #[cfg(feature = "tracing")]
601 tracing::error!(key = %key, "Failed to store refreshed entry");
602 return Err("Failed to store entry".into());
603 }
604 }
605 }
606
607 Ok(())
608 })
609 }
610}
611
612impl<S, B, ReqBody, ResBody> Service<Request<ReqBody>> for CacheService<S, B>
618where
619 S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
620 S::Future: Send + 'static,
621 S::Error: Into<BoxError> + Send,
622 ReqBody: Send + 'static,
623 ResBody: Body<Data = Bytes> + Send + Sync + 'static,
624 ResBody::Error: Into<BoxError> + Send,
625 B: CacheBackend,
626{
627 type Response = Response<SyncBoxBody>;
628 type Error = BoxError;
629 type Future = BoxFuture<Result<Self::Response, Self::Error>>;
630
631 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
632 self.inner.poll_ready(cx).map_err(Into::into)
633 }
634
635 fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
636 let method = req.method().clone();
637 let uri = req.uri().clone();
638 let should_cache_method = self.policy.should_cache_method(&method);
639 let request_bypass =
640 self.policy.respect_cache_control() && cache_control_disallows(req.headers());
641 let key = if should_cache_method && !request_bypass {
642 self.key_extractor.extract(&method, &uri)
643 } else {
644 None
645 };
646
647 let backend = self.backend.clone();
648 let policy = self.policy.clone();
649 let locks = self.locks.clone();
650 let inner = self.inner.clone();
651 let stale_window = policy.stale_while_revalidate();
652 let refresh_before = policy.refresh_before();
653 let refresh_manager = self.refresh_manager.clone();
654 let chunk_cache = self.chunk_cache.clone();
655
656 let range_request = parse_range_header(req.headers());
658
659 let refresh_metadata = if refresh_manager.is_some() && key.is_some() {
661 Some(RefreshMetadata::from_request(&req))
662 } else {
663 None
664 };
665
666 Box::pin(async move {
667 #[cfg(feature = "tracing")]
668 tracing::debug!(method = %method, uri = %uri, "cache_call");
669
670 if let (Some(range_req), Some(chunk_cache), Some(key_ref)) =
672 (range_request.as_ref(), &chunk_cache, &key)
673 {
674 if let Some(entry) = chunk_cache.get(key_ref) {
675 if let Some(normalized) = range_req.normalize(entry.metadata.total_size) {
677 let end = normalized.end.unwrap_or(entry.metadata.total_size - 1);
678
679 if let Some(range_data) = entry.get_range(normalized.start, end) {
681 #[cfg(feature = "metrics")]
682 counter!("tower_http_cache.chunk_cache_hit").increment(1);
683
684 #[cfg(feature = "tracing")]
685 tracing::debug!(
686 key = %key_ref,
687 start = normalized.start,
688 end = end,
689 "chunk_cache_hit"
690 );
691
692 let mut response = Response::builder()
694 .status(http::StatusCode::PARTIAL_CONTENT)
695 .body(SyncBoxBody::new(
696 Full::from(range_data).map_err(Into::into).boxed(),
697 ))
698 .unwrap();
699
700 for (name, value) in &entry.metadata.headers {
702 if let (Ok(header_name), Ok(header_value)) = (
703 http::header::HeaderName::from_bytes(name.as_bytes()),
704 http::header::HeaderValue::from_bytes(value),
705 ) {
706 response.headers_mut().insert(header_name, header_value);
707 }
708 }
709
710 let content_range = format!(
712 "bytes {}-{}/{}",
713 normalized.start, end, entry.metadata.total_size
714 );
715 response.headers_mut().insert(
716 http::header::CONTENT_RANGE,
717 http::header::HeaderValue::from_str(&content_range).unwrap(),
718 );
719
720 let content_length = (end - normalized.start + 1).to_string();
722 response.headers_mut().insert(
723 http::header::CONTENT_LENGTH,
724 http::header::HeaderValue::from_str(&content_length).unwrap(),
725 );
726
727 return Ok(response);
728 }
729 }
730 }
731
732 #[cfg(feature = "metrics")]
733 counter!("tower_http_cache.chunk_cache_miss").increment(1);
734 }
735
736 let mut stale_entry: Option<CacheEntry> = None;
737 if let Some(ref key_ref) = key {
738 if let Ok(Some(hit)) = backend.get(key_ref).await {
739 match classify_hit(hit, stale_window, refresh_before) {
740 HitState::Fresh(entry) => {
741 #[cfg(feature = "metrics")]
742 counter!("tower_http_cache.hit").increment(1);
743
744 if let Some(ref manager) = refresh_manager {
746 manager.tracker().record_hit(key_ref);
747 }
748
749 return Ok(entry.into_response());
750 }
751 HitState::Stale(entry) => {
752 #[cfg(feature = "metrics")]
753 counter!("tower_http_cache.stale_hit").increment(1);
754
755 if let Some(ref manager) = refresh_manager {
757 manager.tracker().record_hit(key_ref);
758 }
759
760 stale_entry = Some(entry);
761 }
762 HitState::Expired => {}
763 }
764 }
765 }
766
767 let mut primary_guard: Option<StampedeGuard> = None;
768 if let Some(ref key_ref) = key {
769 match StampedeGuard::acquire_handle(locks.clone(), key_ref.clone()).await {
770 StampedeHandle::Primary(guard) => {
771 primary_guard = Some(guard);
772 }
773 StampedeHandle::Secondary(lock) => {
774 if let Some(entry) = stale_entry.clone() {
775 #[cfg(feature = "metrics")]
776 counter!("tower_http_cache.stale_served").increment(1);
777 return Ok(entry.into_response());
778 }
779
780 let secondary_guard = lock.lock_owned().await;
781 drop(secondary_guard);
782
783 if let Ok(Some(hit)) = backend.get(key_ref).await {
784 match classify_hit(hit, stale_window, refresh_before) {
785 HitState::Fresh(entry) => {
786 #[cfg(feature = "metrics")]
787 counter!("tower_http_cache.hit_after_wait").increment(1);
788 return Ok(entry.into_response());
789 }
790 HitState::Stale(entry) => {
791 #[cfg(feature = "metrics")]
792 counter!("tower_http_cache.stale_served").increment(1);
793 return Ok(entry.into_response());
794 }
795 HitState::Expired => {}
796 }
797 }
798
799 if let StampedeHandle::Primary(guard) =
800 StampedeGuard::acquire_handle(locks.clone(), key_ref.clone()).await
801 {
802 primary_guard = Some(guard);
803 }
804 }
805 }
806
807 if primary_guard.is_some() {
817 if let Ok(Some(hit)) = backend.get(key_ref).await {
818 if let HitState::Fresh(entry) =
819 classify_hit(hit, stale_window, refresh_before)
820 {
821 #[cfg(feature = "metrics")]
822 counter!("tower_http_cache.hit_after_lock").increment(1);
823 return Ok(entry.into_response());
824 }
825 }
826 }
827 }
828
829 #[cfg(feature = "metrics")]
830 counter!("tower_http_cache.miss").increment(1);
831
832 #[cfg(feature = "metrics")]
833 let start = std::time::Instant::now();
834 let service = inner;
835 let response = service.oneshot(req).await.map_err(|err| err.into())?;
836 #[cfg(feature = "metrics")]
837 histogram!("tower_http_cache.backend_latency").record(start.elapsed().as_secs_f64());
838
839 let (parts, body) = response.into_parts();
840
841 let content_type = parts
843 .headers
844 .get(http::header::CONTENT_TYPE)
845 .and_then(|v| v.to_str().ok());
846
847 let content_length = parts
848 .headers
849 .get(http::header::CONTENT_LENGTH)
850 .and_then(|v| v.to_str().ok())
851 .and_then(|v| v.parse::<u64>().ok());
852
853 let size_hint = body.size_hint();
854
855 let streaming_decision = should_stream(
856 policy.streaming_policy(),
857 &size_hint,
858 content_type,
859 content_length,
860 );
861
862 let is_range_request = parse_range_header(&parts.headers).is_some();
864 let is_partial_response = is_partial_content(parts.status);
865 let range_handling = policy.streaming_policy().range_handling;
866
867 if (is_range_request || is_partial_response)
869 && range_handling == RangeHandling::PassThrough
870 {
871 #[cfg(feature = "metrics")]
872 counter!("tower_http_cache.range_request_passthrough").increment(1);
873
874 #[cfg(feature = "tracing")]
875 tracing::debug!(
876 method = %method,
877 uri = %uri,
878 is_range = is_range_request,
879 is_partial = is_partial_response,
880 "range_request_passthrough"
881 );
882
883 let boxed_body = SyncBoxBody::new(body.map_err(Into::into).boxed());
885 drop(primary_guard);
886 return Ok(Response::from_parts(parts, boxed_body));
887 }
888
889 match streaming_decision {
891 StreamingDecision::SkipCache | StreamingDecision::StreamThrough => {
892 #[cfg(feature = "metrics")]
894 counter!("tower_http_cache.streaming_passthrough").increment(1);
895
896 #[cfg(feature = "tracing")]
897 tracing::debug!(
898 method = %method,
899 uri = %uri,
900 decision = ?streaming_decision,
901 content_type = ?content_type,
902 size = ?extract_size_info(&size_hint, content_length),
903 "streaming_passthrough"
904 );
905
906 let boxed_body = SyncBoxBody::new(body.map_err(Into::into).boxed());
908 drop(primary_guard);
909 return Ok(Response::from_parts(parts, boxed_body));
910 }
911 _ => {}
912 }
913
914 let streaming = body.size_hint().upper().is_none();
915 if streaming && !policy.allow_streaming_bodies() {
916 #[cfg(feature = "metrics")]
917 counter!("tower_http_cache.streaming_skip").increment(1);
918 }
919
920 let collected = BodyExt::collect(body).await.map_err(|err| err.into())?;
921 let cache_bytes = collected.to_bytes();
922 let response_bytes = cache_bytes.clone();
923
924 if let (Some(chunk_cache), Some(key_ref)) = (&chunk_cache, &key) {
926 let streaming_policy = policy.streaming_policy();
927
928 if streaming_policy.enable_chunk_cache
929 && cache_bytes.len() as u64 >= streaming_policy.min_chunk_file_size
930 && parts.status.is_success()
931 {
932 #[cfg(feature = "tracing")]
933 tracing::debug!(
934 key = %key_ref,
935 size = cache_bytes.len(),
936 chunk_size = streaming_policy.chunk_size,
937 "populating_chunk_cache"
938 );
939
940 let metadata = ChunkMetadata {
942 total_size: cache_bytes.len() as u64,
943 content_type: parts
944 .headers
945 .get(http::header::CONTENT_TYPE)
946 .and_then(|v| v.to_str().ok())
947 .unwrap_or("application/octet-stream")
948 .to_string(),
949 etag: parts
950 .headers
951 .get(http::header::ETAG)
952 .and_then(|v| v.to_str().ok())
953 .map(|s| s.to_string()),
954 last_modified: parts
955 .headers
956 .get(http::header::LAST_MODIFIED)
957 .and_then(|v| v.to_str().ok())
958 .map(|s| s.to_string()),
959 status: parts.status,
960 version: parts.version,
961 headers: policy.headers_to_cache(&parts.headers),
962 };
963
964 let entry = chunk_cache.get_or_create(key_ref.clone(), metadata);
966
967 let chunk_size = streaming_policy.chunk_size;
969 let mut offset = 0;
970 let mut chunk_index = 0;
971
972 while offset < cache_bytes.len() {
973 let end = std::cmp::min(offset + chunk_size, cache_bytes.len());
974 let chunk = cache_bytes.slice(offset..end);
975 entry.add_chunk(chunk_index, chunk);
976
977 offset = end;
978 chunk_index += 1;
979 }
980
981 #[cfg(feature = "metrics")]
982 counter!("tower_http_cache.chunk_cache_stored").increment(1);
983
984 #[cfg(feature = "tracing")]
985 tracing::debug!(
986 key = %key_ref,
987 chunks = chunk_index,
988 "chunk_cache_populated"
989 );
990 }
991 }
992
993 let cache_control_block =
994 policy.respect_cache_control() && cache_control_disallows(&parts.headers);
995 let body_too_large = policy
996 .max_body_size()
997 .is_some_and(|max| cache_bytes.len() > max);
998 let body_too_small = policy
999 .min_body_size()
1000 .is_some_and(|min| cache_bytes.len() < min);
1001
1002 let should_store = key.is_some()
1003 && !cache_control_block
1004 && !body_too_large
1005 && !body_too_small
1006 && (policy.allow_streaming_bodies() || !streaming);
1007
1008 let headers_to_cache = if should_store {
1009 Some(policy.headers_to_cache(&parts.headers))
1010 } else {
1011 None
1012 };
1013
1014 let version = parts.version;
1015 let status = parts.status;
1016
1017 if should_store {
1018 if let Some(key_ref) = &key {
1019 if let Some(ttl) = policy.ttl_for(status) {
1020 if !ttl.is_zero() {
1021 let stale_for = policy.stale_while_revalidate();
1022 let (compressed_bytes, compressed) =
1023 maybe_compress(cache_bytes.clone(), policy.compression());
1024 if compressed {
1025 #[cfg(feature = "metrics")]
1026 counter!("tower_http_cache.compressed").increment(1);
1027 }
1028 let entry = CacheEntry::new(
1029 status,
1030 version,
1031 headers_to_cache.unwrap(),
1032 compressed_bytes,
1033 );
1034 let tags = policy.extract_tags(&method, &uri);
1039 let entry = if tags.is_empty() {
1040 entry
1041 } else {
1042 entry.with_tags(tags)
1043 };
1044 if backend
1045 .set(key_ref.clone(), entry, ttl, stale_for)
1046 .await
1047 .is_err()
1048 {
1049 #[cfg(feature = "metrics")]
1050 counter!("tower_http_cache.store_error").increment(1);
1051 } else {
1052 #[cfg(feature = "metrics")]
1053 counter!("tower_http_cache.store").increment(1);
1054
1055 if let (Some(manager), Some(metadata)) =
1057 (&refresh_manager, refresh_metadata)
1058 {
1059 manager.store_metadata(key_ref.clone(), metadata);
1060 }
1061 }
1062 }
1063 }
1064 }
1065 } else {
1066 #[cfg(feature = "metrics")]
1067 counter!("tower_http_cache.store_skipped").increment(1);
1068 }
1069
1070 drop(primary_guard);
1071
1072 let full_body = Full::from(response_bytes);
1074 let boxed_body = SyncBoxBody::new(full_body.map_err(Into::into).boxed());
1075 Ok(Response::from_parts(parts, boxed_body))
1076 })
1077 }
1078}
1079
1080struct StampedeGuard {
1081 key: String,
1082 locks: Arc<DashMap<String, Arc<Mutex<()>>>>,
1083 lock: Arc<Mutex<()>>,
1084 _guard: OwnedMutexGuard<()>,
1085}
1086
1087enum StampedeHandle {
1088 Primary(StampedeGuard),
1089 Secondary(Arc<Mutex<()>>),
1090}
1091
1092impl StampedeGuard {
1093 async fn acquire_handle(
1094 locks: Arc<DashMap<String, Arc<Mutex<()>>>>,
1095 key: String,
1096 ) -> StampedeHandle {
1097 #[allow(clippy::let_and_return)]
1107 let handle = match locks.entry(key.clone()) {
1108 Entry::Occupied(entry) => StampedeHandle::Secondary(entry.get().clone()),
1109 Entry::Vacant(entry) => {
1110 let lock = Arc::new(Mutex::new(()));
1111 entry.insert(lock.clone());
1112 let guard = lock.clone().lock_owned().await;
1113 let locks_clone = locks.clone();
1114 StampedeHandle::Primary(StampedeGuard {
1115 key,
1116 locks: locks_clone,
1117 lock,
1118 _guard: guard,
1119 })
1120 }
1121 };
1122 handle
1123 }
1124}
1125
1126impl Drop for StampedeGuard {
1127 fn drop(&mut self) {
1128 if let Some(current) = self.locks.get(&self.key) {
1129 let should_remove = Arc::ptr_eq(&self.lock, current.value());
1130 drop(current);
1131 if should_remove {
1132 self.locks.remove(&self.key);
1133 }
1134 }
1135 }
1136}
1137
1138fn classify_hit(hit: CacheRead, stale_window: Duration, refresh_before: Duration) -> HitState {
1139 let now = SystemTime::now();
1140 let CacheRead {
1141 entry,
1142 expires_at,
1143 stale_until,
1144 } = hit;
1145
1146 if let Some(expires_at) = expires_at {
1147 if expires_at > now {
1148 if refresh_before > Duration::ZERO {
1149 if let Some(threshold) = expires_at.checked_sub(refresh_before) {
1150 if now >= threshold {
1151 return HitState::Stale(entry);
1152 }
1153 } else {
1154 return HitState::Stale(entry);
1155 }
1156 }
1157 return HitState::Fresh(entry);
1158 }
1159 }
1160
1161 if stale_window > Duration::ZERO {
1162 if let Some(stale_until) = stale_until {
1163 if stale_until > now {
1164 return HitState::Stale(entry);
1165 }
1166 }
1167 }
1168
1169 HitState::Expired
1170}
1171
1172#[derive(Debug)]
1173enum HitState {
1174 Fresh(CacheEntry),
1175 Stale(CacheEntry),
1176 Expired,
1177}
1178
1179fn cache_control_disallows(headers: &HeaderMap) -> bool {
1180 headers
1181 .get_all(CACHE_CONTROL)
1182 .iter()
1183 .filter_map(|value| value.to_str().ok())
1184 .flat_map(|value| value.split(','))
1185 .map(|token| token.trim().to_ascii_lowercase())
1186 .any(|token| matches!(token.as_str(), "no-store" | "no-cache" | "private"))
1187 || headers
1188 .get(PRAGMA)
1189 .and_then(|value| value.to_str().ok())
1190 .map(|value| value.to_ascii_lowercase().contains("no-cache"))
1191 .unwrap_or(false)
1192}
1193
1194#[cfg(feature = "compression")]
1195fn maybe_compress(bytes: Bytes, config: CompressionConfig) -> (Bytes, bool) {
1196 use flate2::{Compression, write::GzEncoder};
1197 use std::io::Write;
1198
1199 match config.strategy {
1200 CompressionStrategy::None => (bytes, false),
1201 CompressionStrategy::Gzip => {
1202 if bytes.len() < config.min_size {
1203 return (bytes, false);
1204 }
1205 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1206 if encoder.write_all(&bytes).is_err() {
1207 return (bytes, false);
1208 }
1209 match encoder.finish() {
1210 Ok(data) => (Bytes::from(data), true),
1211 Err(_) => (bytes, false),
1212 }
1213 }
1214 }
1215}
1216
1217#[cfg(not(feature = "compression"))]
1218fn maybe_compress(bytes: Bytes, _config: CompressionConfig) -> (Bytes, bool) {
1219 let _ = _config;
1220 (bytes, false)
1221}
1222
1223#[cfg(all(test, feature = "in-memory"))]
1224mod tests {
1225 use super::*;
1226 use crate::backend::CacheEntry;
1227 use bytes::Bytes;
1228 use http::{HeaderValue, StatusCode, Version};
1229 use tokio::task::yield_now;
1230
1231 fn mock_entry() -> CacheEntry {
1232 CacheEntry::new(
1233 StatusCode::OK,
1234 Version::HTTP_11,
1235 Vec::new(),
1236 Bytes::from_static(b"body"),
1237 )
1238 }
1239
1240 #[test]
1241 fn classify_hit_marks_entry_fresh_when_not_near_expiry() {
1242 let now = SystemTime::now();
1243 let hit = CacheRead {
1244 entry: mock_entry(),
1245 expires_at: Some(now + Duration::from_secs(10)),
1246 stale_until: Some(now + Duration::from_secs(20)),
1247 };
1248
1249 match classify_hit(hit, Duration::from_secs(5), Duration::from_secs(1)) {
1250 HitState::Fresh(_) => {}
1251 other => panic!("expected fresh entry, got {:?}", other),
1252 }
1253 }
1254
1255 #[test]
1256 fn classify_hit_marks_entry_stale_when_within_refresh_window() {
1257 let now = SystemTime::now();
1258 let hit = CacheRead {
1259 entry: mock_entry(),
1260 expires_at: Some(now + Duration::from_secs(2)),
1261 stale_until: Some(now + Duration::from_secs(10)),
1262 };
1263
1264 match classify_hit(hit, Duration::from_secs(5), Duration::from_secs(5)) {
1265 HitState::Stale(_) => {}
1266 other => panic!("expected stale entry, got {:?}", other),
1267 }
1268 }
1269
1270 #[test]
1271 fn classify_hit_marks_entry_stale_when_within_stale_window() {
1272 let now = SystemTime::now();
1273 let hit = CacheRead {
1274 entry: mock_entry(),
1275 expires_at: Some(now - Duration::from_secs(1)),
1276 stale_until: Some(now + Duration::from_secs(1)),
1277 };
1278
1279 match classify_hit(hit, Duration::from_secs(2), Duration::from_secs(0)) {
1280 HitState::Stale(_) => {}
1281 other => panic!("expected stale entry, got {:?}", other),
1282 }
1283 }
1284
1285 #[test]
1286 fn classify_hit_marks_entry_expired_after_stale_window() {
1287 let now = SystemTime::now();
1288 let hit = CacheRead {
1289 entry: mock_entry(),
1290 expires_at: Some(now - Duration::from_secs(5)),
1291 stale_until: Some(now - Duration::from_secs(1)),
1292 };
1293
1294 match classify_hit(hit, Duration::from_secs(5), Duration::from_secs(0)) {
1295 HitState::Expired => {}
1296 other => panic!("expected expired entry, got {:?}", other),
1297 }
1298 }
1299
1300 #[test]
1301 fn cache_control_disallows_detects_no_cache_directives() {
1302 let mut headers = HeaderMap::new();
1303 headers.insert(
1304 CACHE_CONTROL,
1305 HeaderValue::from_static("max-age=0, no-cache"),
1306 );
1307 assert!(cache_control_disallows(&headers));
1308
1309 let mut pragma_only = HeaderMap::new();
1310 pragma_only.insert(PRAGMA, HeaderValue::from_static("no-cache"));
1311 assert!(cache_control_disallows(&pragma_only));
1312 }
1313
1314 #[tokio::test]
1315 async fn stampede_guard_drop_removes_lock_entry() {
1316 let locks = Arc::new(DashMap::new());
1317 let key = "key".to_string();
1318
1319 match StampedeGuard::acquire_handle(locks.clone(), key.clone()).await {
1320 StampedeHandle::Primary(guard) => {
1321 assert!(locks.get(&key).is_some());
1322 drop(guard);
1323 yield_now().await;
1324 assert!(locks.get(&key).is_none());
1325 }
1326 StampedeHandle::Secondary(_) => panic!("expected primary guard"),
1327 }
1328 }
1329
1330 #[test]
1331 fn cache_service_implements_clone() {
1332 use crate::backend::memory::InMemoryBackend;
1333 use tower::service_fn;
1334
1335 fn assert_clone<T: Clone>(_: &T) {}
1337
1338 let backend = InMemoryBackend::new(100);
1339 let layer = CacheLayer::new(backend);
1340 let service = layer.layer(service_fn(|_req: http::Request<()>| async {
1341 Ok::<_, std::convert::Infallible>(http::Response::new(()))
1342 }));
1343
1344 assert_clone(&service);
1346 }
1347}