http_cache_tower_server/lib.rs
1//! Server-side HTTP response caching middleware for Tower.
2//!
3//! This crate provides Tower middleware for caching HTTP responses on the server side.
4//! Unlike client-side caching, this middleware caches your own application's responses
5//! to reduce load and improve performance.
6//!
7//! # Key Features
8//!
9//! - Response-first architecture: Caches based on response headers, not requests
10//! - Preserves request context: Maintains all request extensions (path params, state, etc.)
11//! - Handler-centric: Calls the handler first, then decides whether to cache
12//! - RFC 7234 compliant: Respects Cache-Control, Vary, and other standard headers
13//! - Reuses existing infrastructure: Leverages `CacheManager` trait from `http-cache`
14//!
15//! # Example
16//!
17//! ```rust
18//! use http::{Request, Response};
19//! use http_body_util::Full;
20//! use bytes::Bytes;
21//! use http_cache_tower_server::ServerCacheLayer;
22//! use tower::{Service, Layer};
23//! # use http_cache::{CacheManager, HttpResponse, HttpVersion};
24//! # use http_cache_semantics::CachePolicy;
25//! # use std::collections::HashMap;
26//! # use std::sync::{Arc, Mutex};
27//! #
28//! # #[derive(Clone)]
29//! # struct MemoryCacheManager {
30//! # store: Arc<Mutex<HashMap<String, (HttpResponse, CachePolicy)>>>,
31//! # }
32//! #
33//! # impl MemoryCacheManager {
34//! # fn new() -> Self {
35//! # Self { store: Arc::new(Mutex::new(HashMap::new())) }
36//! # }
37//! # }
38//! #
39//! # impl CacheManager for MemoryCacheManager {
40//! # async fn get(&self, cache_key: &str) -> http_cache::Result<Option<(HttpResponse, CachePolicy)>> {
41//! # Ok(self.store.lock().unwrap().get(cache_key).cloned())
42//! # }
43//! # async fn put(&self, cache_key: String, res: HttpResponse, policy: CachePolicy) -> http_cache::Result<HttpResponse> {
44//! # self.store.lock().unwrap().insert(cache_key, (res.clone(), policy));
45//! # Ok(res)
46//! # }
47//! # async fn delete(&self, cache_key: &str) -> http_cache::Result<()> {
48//! # self.store.lock().unwrap().remove(cache_key);
49//! # Ok(())
50//! # }
51//! # }
52//!
53//! # tokio_test::block_on(async {
54//! let manager = MemoryCacheManager::new();
55//! let layer = ServerCacheLayer::new(manager);
56//!
57//! // Apply the layer to your Tower service
58//! let service = tower::service_fn(|_req: Request<Full<Bytes>>| async {
59//! Ok::<_, std::io::Error>(
60//! Response::builder()
61//! .header("cache-control", "max-age=60")
62//! .body(Full::new(Bytes::from("Hello, World!")))
63//! .unwrap()
64//! )
65//! });
66//!
67//! let mut cached_service = layer.layer(service);
68//! # });
69//! ```
70//!
71//! # Vary Header Support
72//!
73//! This cache enforces `Vary` headers using `http-cache-semantics`. When a response includes
74//! a `Vary` header, subsequent requests must have matching header values to receive the cached
75//! response. Requests with different header values will result in cache misses.
76//!
77//! For example, if a response has `Vary: Accept-Language`, a cached English response won't be
78//! served to a request with `Accept-Language: de`.
79//!
80//! # Security Warnings
81//!
82//! This is a **shared cache** - cached responses are served to ALL users. Improper configuration
83//! can leak user-specific data between different users.
84//!
85//! ## Authorization and Authentication
86//!
87//! This cache does not check for `Authorization` headers or session cookies in requests.
88//! Caching authenticated endpoints without proper cache key differentiation will cause
89//! user A's response to be served to user B.
90//!
91//! **Do NOT cache authenticated endpoints** unless you use a `CustomKeyer` that includes
92//! the user or session identifier in the cache key:
93//!
94//! ```rust
95//! # use http_cache_tower_server::CustomKeyer;
96//! # use http::Request;
97//! // Example: Include session ID in cache key
98//! let keyer = CustomKeyer::new(|req: &Request<()>| {
99//! let session = req.headers()
100//! .get("cookie")
101//! .and_then(|v| v.to_str().ok())
102//! .and_then(|c| extract_session_id(c))
103//! .unwrap_or("anonymous");
104//! format!("{} {} session:{}", req.method(), req.uri().path(), session)
105//! });
106//! # fn extract_session_id(cookie: &str) -> Option<&str> { None }
107//! ```
108//!
109//! ## General Security Considerations
110//!
111//! - Never cache responses containing user-specific data without user-specific cache keys
112//! - Validate cache keys to prevent cache poisoning attacks
113//! - Be careful with header-based caching due to header injection risks
114//! - Consider the `private` Cache-Control directive for user-specific responses (automatically rejected by this cache)
115
116#![warn(missing_docs)]
117#![deny(unsafe_code)]
118
119use bytes::Bytes;
120use http::{header::HeaderValue, Request, Response};
121use http_body::{Body as HttpBody, Frame};
122use http_body_util::BodyExt;
123use http_cache::{CacheManager, HttpResponse, HttpVersion};
124use http_cache_semantics::{BeforeRequest, CachePolicy};
125use serde::{Deserialize, Serialize};
126use std::collections::HashMap;
127use std::error::Error as StdError;
128use std::pin::Pin;
129use std::sync::atomic::{AtomicU64, Ordering};
130use std::sync::Arc;
131use std::task::{Context, Poll};
132use std::time::{Duration, SystemTime};
133use tower::{Layer, Service};
134
135type BoxError = Box<dyn StdError + Send + Sync>;
136
137/// Cache performance metrics.
138///
139/// Tracks hits, misses, and stores for monitoring cache effectiveness.
140#[derive(Debug, Default)]
141pub struct CacheMetrics {
142 /// Number of cache hits.
143 pub hits: AtomicU64,
144 /// Number of cache misses.
145 pub misses: AtomicU64,
146 /// Number of responses stored in cache.
147 pub stores: AtomicU64,
148 /// Number of responses skipped (too large, not cacheable, etc.).
149 pub skipped: AtomicU64,
150}
151
152impl CacheMetrics {
153 /// Create new metrics instance.
154 pub fn new() -> Self {
155 Self::default()
156 }
157
158 /// Calculate cache hit rate as a percentage (0.0 to 1.0).
159 pub fn hit_rate(&self) -> f64 {
160 let hits = self.hits.load(Ordering::Relaxed);
161 let total = hits + self.misses.load(Ordering::Relaxed);
162 if total == 0 {
163 0.0
164 } else {
165 hits as f64 / total as f64
166 }
167 }
168
169 /// Reset all metrics to zero.
170 pub fn reset(&self) {
171 self.hits.store(0, Ordering::Relaxed);
172 self.misses.store(0, Ordering::Relaxed);
173 self.stores.store(0, Ordering::Relaxed);
174 self.skipped.store(0, Ordering::Relaxed);
175 }
176}
177
178/// A trait for generating cache keys from HTTP requests.
179pub trait Keyer: Clone + Send + Sync + 'static {
180 /// Generate a cache key for the given request.
181 fn cache_key<B>(&self, req: &Request<B>) -> String;
182}
183
184/// Default keyer that uses HTTP method and path.
185///
186/// Generates keys in the format: `{METHOD} {path}`
187///
188/// # Example
189///
190/// ```
191/// # use http::Request;
192/// # use http_cache_tower_server::{Keyer, DefaultKeyer};
193/// let keyer = DefaultKeyer;
194/// let req = Request::get("/users/123").body(()).unwrap();
195/// let key = keyer.cache_key(&req);
196/// assert_eq!(key, "GET /users/123");
197/// ```
198#[derive(Debug, Clone, Copy, Default)]
199pub struct DefaultKeyer;
200
201impl Keyer for DefaultKeyer {
202 fn cache_key<B>(&self, req: &Request<B>) -> String {
203 format!("{} {}", req.method(), req.uri().path())
204 }
205}
206
207/// Keyer that includes query parameters in the cache key.
208///
209/// Generates keys in the format: `{METHOD} {path}?{query}`
210///
211/// # Example
212///
213/// ```
214/// # use http::Request;
215/// # use http_cache_tower_server::{Keyer, QueryKeyer};
216/// let keyer = QueryKeyer;
217/// let req = Request::get("/users?page=1").body(()).unwrap();
218/// let key = keyer.cache_key(&req);
219/// assert_eq!(key, "GET /users?page=1");
220/// ```
221#[derive(Debug, Clone, Copy, Default)]
222pub struct QueryKeyer;
223
224impl Keyer for QueryKeyer {
225 fn cache_key<B>(&self, req: &Request<B>) -> String {
226 format!("{} {}", req.method(), req.uri())
227 }
228}
229
230/// Custom keyer that uses a user-provided function.
231///
232/// Use this when the default method+path keying is insufficient, such as:
233/// - Content negotiation based on request headers (Accept-Language, Accept-Encoding)
234/// - User-specific or session-specific caching
235/// - Query parameter normalization
236///
237/// # Examples
238///
239/// Basic custom format:
240///
241/// ```
242/// # use http::Request;
243/// # use http_cache_tower_server::{Keyer, CustomKeyer};
244/// let keyer = CustomKeyer::new(|req: &Request<()>| {
245/// format!("custom-{}-{}", req.method(), req.uri().path())
246/// });
247/// let req = Request::get("/users").body(()).unwrap();
248/// let key = keyer.cache_key(&req);
249/// assert_eq!(key, "custom-GET-/users");
250/// ```
251///
252/// Content negotiation (Accept-Language):
253///
254/// ```
255/// # use http::Request;
256/// # use http_cache_tower_server::{Keyer, CustomKeyer};
257/// let keyer = CustomKeyer::new(|req: &Request<()>| {
258/// let lang = req.headers()
259/// .get("accept-language")
260/// .and_then(|v| v.to_str().ok())
261/// .and_then(|s| s.split(',').next())
262/// .unwrap_or("en");
263/// format!("{} {} lang:{}", req.method(), req.uri().path(), lang)
264/// });
265/// ```
266///
267/// User-specific caching (session-based):
268///
269/// ```
270/// # use http::Request;
271/// # use http_cache_tower_server::{Keyer, CustomKeyer};
272/// let keyer = CustomKeyer::new(|req: &Request<()>| {
273/// let user_id = req.headers()
274/// .get("x-user-id")
275/// .and_then(|v| v.to_str().ok())
276/// .unwrap_or("anonymous");
277/// format!("{} {} user:{}", req.method(), req.uri().path(), user_id)
278/// });
279/// ```
280///
281/// # Security Warning
282///
283/// When caching user-specific or session-specific data, ensure the user/session identifier
284/// is included in the cache key. Failure to do so will cause responses from one user to be
285/// served to other users.
286#[derive(Clone)]
287pub struct CustomKeyer<F> {
288 func: F,
289}
290
291impl<F> CustomKeyer<F> {
292 /// Create a new custom keyer with the given function.
293 pub fn new(func: F) -> Self {
294 Self { func }
295 }
296}
297
298impl<F> Keyer for CustomKeyer<F>
299where
300 F: Fn(&Request<()>) -> String + Clone + Send + Sync + 'static,
301{
302 fn cache_key<B>(&self, req: &Request<B>) -> String {
303 // Create a temporary request with the same parts but () body
304 let mut temp_req = Request::builder()
305 .method(req.method())
306 .uri(req.uri())
307 .version(req.version())
308 .body(())
309 .expect("request built from valid parts");
310
311 // Copy headers for content negotiation support
312 *temp_req.headers_mut() = req.headers().clone();
313
314 (self.func)(&temp_req)
315 }
316}
317
318/// Configuration options for server-side caching.
319#[derive(Debug, Clone)]
320pub struct ServerCacheOptions {
321 /// Default TTL when response has no Cache-Control header.
322 pub default_ttl: Option<Duration>,
323
324 /// Maximum TTL, even if response specifies longer.
325 pub max_ttl: Option<Duration>,
326
327 /// Minimum TTL, even if response specifies shorter.
328 pub min_ttl: Option<Duration>,
329
330 /// Whether to add X-Cache headers (HIT/MISS).
331 pub cache_status_headers: bool,
332
333 /// Maximum response body size to cache (in bytes).
334 pub max_body_size: usize,
335
336 /// Whether to cache responses without explicit Cache-Control.
337 pub cache_by_default: bool,
338
339 /// Whether to respect Vary header for content negotiation.
340 ///
341 /// When true (default), cached responses are only served if the request's
342 /// headers match those specified in the response's Vary header. This is
343 /// enforced via `http-cache-semantics`.
344 pub respect_vary: bool,
345
346 /// Whether to respect Authorization headers per RFC 9111 §3.5.
347 ///
348 /// When true (default), requests with `Authorization` headers are not cached
349 /// unless the response explicitly permits it via `public`, `s-maxage`, or
350 /// `must-revalidate` directives.
351 ///
352 /// This prevents accidental caching of authenticated responses that could
353 /// leak user-specific data to other users.
354 pub respect_authorization: bool,
355}
356
357impl Default for ServerCacheOptions {
358 fn default() -> Self {
359 Self {
360 default_ttl: Some(Duration::from_secs(60)),
361 max_ttl: Some(Duration::from_secs(3600)),
362 min_ttl: None,
363 cache_status_headers: true,
364 max_body_size: 128 * 1024 * 1024,
365 cache_by_default: false,
366 respect_vary: true,
367 respect_authorization: true,
368 }
369 }
370}
371
372/// A cached HTTP response with metadata.
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct CachedResponse {
375 /// Response status code.
376 pub status: u16,
377
378 /// Response headers (multiple values per key to preserve multi-valued headers).
379 pub headers: HashMap<String, Vec<String>>,
380
381 /// Response body bytes.
382 pub body: Vec<u8>,
383
384 /// When this response was cached.
385 pub cached_at: SystemTime,
386
387 /// Time-to-live duration.
388 pub ttl: Duration,
389
390 /// Optional vary headers for content negotiation.
391 pub vary: Option<Vec<String>>,
392}
393
394impl CachedResponse {
395 /// Check if this cached response is stale.
396 pub fn is_stale(&self) -> bool {
397 SystemTime::now()
398 .duration_since(self.cached_at)
399 .unwrap_or(Duration::MAX)
400 > self.ttl
401 }
402
403 /// Convert to an HTTP response.
404 pub fn into_response(
405 self,
406 ) -> Result<Response<Bytes>, Box<dyn std::error::Error + Send + Sync>> {
407 let mut builder = Response::builder().status(self.status);
408
409 for (key, values) in self.headers {
410 for value in values {
411 if let Ok(header_value) = HeaderValue::from_str(&value) {
412 builder = builder.header(&key, header_value);
413 }
414 }
415 }
416
417 Ok(builder.body(Bytes::from(self.body))?)
418 }
419}
420
421/// Response body types.
422#[derive(Debug)]
423pub enum ResponseBody {
424 /// Cached response body.
425 Cached(Bytes),
426 /// Fresh response body.
427 Fresh(Bytes),
428 /// Uncacheable response body.
429 Uncacheable(Bytes),
430}
431
432impl HttpBody for ResponseBody {
433 type Data = Bytes;
434 type Error = BoxError;
435
436 fn poll_frame(
437 mut self: Pin<&mut Self>,
438 _cx: &mut Context<'_>,
439 ) -> Poll<Option<std::result::Result<Frame<Self::Data>, Self::Error>>> {
440 let bytes = match &mut *self {
441 ResponseBody::Cached(b)
442 | ResponseBody::Fresh(b)
443 | ResponseBody::Uncacheable(b) => {
444 std::mem::replace(b, Bytes::new())
445 }
446 };
447
448 if bytes.is_empty() {
449 Poll::Ready(None)
450 } else {
451 Poll::Ready(Some(Ok(Frame::data(bytes))))
452 }
453 }
454
455 fn is_end_stream(&self) -> bool {
456 match self {
457 ResponseBody::Cached(b)
458 | ResponseBody::Fresh(b)
459 | ResponseBody::Uncacheable(b) => b.is_empty(),
460 }
461 }
462}
463
464/// Tower layer for server-side HTTP response caching.
465///
466/// This layer should be placed AFTER routing to ensure request
467/// extensions (like path parameters) are preserved.
468///
469/// # Shared Cache Behavior
470///
471/// This implements a **shared cache** as defined in RFC 9111. Responses cached by this layer
472/// are served to all users making requests with matching cache keys. The cache automatically
473/// rejects responses with the `private` directive, but does not inspect `Authorization` headers
474/// or session cookies.
475///
476/// For authenticated or user-specific endpoints, either:
477/// - Set `Cache-Control: private` in responses (prevents caching)
478/// - Use a `CustomKeyer` that includes user/session identifiers in the cache key
479#[derive(Clone)]
480pub struct ServerCacheLayer<M, K = DefaultKeyer>
481where
482 M: CacheManager,
483 K: Keyer,
484{
485 manager: M,
486 keyer: K,
487 options: ServerCacheOptions,
488 metrics: Arc<CacheMetrics>,
489}
490
491impl<M> ServerCacheLayer<M, DefaultKeyer>
492where
493 M: CacheManager,
494{
495 /// Create a new cache layer with default options.
496 pub fn new(manager: M) -> Self {
497 Self {
498 manager,
499 keyer: DefaultKeyer,
500 options: ServerCacheOptions::default(),
501 metrics: Arc::new(CacheMetrics::new()),
502 }
503 }
504}
505
506impl<M, K> ServerCacheLayer<M, K>
507where
508 M: CacheManager,
509 K: Keyer,
510{
511 /// Create a cache layer with a custom keyer.
512 pub fn with_keyer(manager: M, keyer: K) -> Self {
513 Self {
514 manager,
515 keyer,
516 options: ServerCacheOptions::default(),
517 metrics: Arc::new(CacheMetrics::new()),
518 }
519 }
520
521 /// Set custom options.
522 pub fn with_options(mut self, options: ServerCacheOptions) -> Self {
523 self.options = options;
524 self
525 }
526
527 /// Get a reference to the cache metrics.
528 pub fn metrics(&self) -> &Arc<CacheMetrics> {
529 &self.metrics
530 }
531
532 /// Invalidate a specific cache entry by its key.
533 pub async fn invalidate(&self, cache_key: &str) -> Result<(), BoxError> {
534 self.manager.delete(cache_key).await
535 }
536
537 /// Invalidate cache entry for a specific request.
538 ///
539 /// Uses the configured keyer to generate the cache key from the request.
540 pub async fn invalidate_request<B>(
541 &self,
542 req: &Request<B>,
543 ) -> Result<(), BoxError> {
544 let cache_key = self.keyer.cache_key(req);
545 self.invalidate(&cache_key).await
546 }
547}
548
549impl<S, M, K> Layer<S> for ServerCacheLayer<M, K>
550where
551 M: CacheManager + Clone,
552 K: Keyer,
553{
554 type Service = ServerCacheService<S, M, K>;
555
556 fn layer(&self, inner: S) -> Self::Service {
557 ServerCacheService {
558 inner,
559 manager: self.manager.clone(),
560 keyer: self.keyer.clone(),
561 options: self.options.clone(),
562 metrics: self.metrics.clone(),
563 }
564 }
565}
566
567/// Tower service that implements response caching.
568#[derive(Clone)]
569pub struct ServerCacheService<S, M, K>
570where
571 M: CacheManager,
572 K: Keyer,
573{
574 inner: S,
575 manager: M,
576 keyer: K,
577 options: ServerCacheOptions,
578 metrics: Arc<CacheMetrics>,
579}
580
581impl<S, ReqBody, ResBody, M, K> Service<Request<ReqBody>>
582 for ServerCacheService<S, M, K>
583where
584 S: Service<Request<ReqBody>, Response = Response<ResBody>>
585 + Clone
586 + Send
587 + 'static,
588 S::Error: Into<BoxError>,
589 S::Future: Send + 'static,
590 M: CacheManager + Clone,
591 K: Keyer,
592 ReqBody: Send + 'static,
593 ResBody: HttpBody + Send + 'static,
594 ResBody::Data: Send,
595 ResBody::Error: Into<BoxError>,
596{
597 type Response = Response<ResponseBody>;
598 type Error = BoxError;
599 type Future = Pin<
600 Box<
601 dyn std::future::Future<
602 Output = std::result::Result<Self::Response, Self::Error>,
603 > + Send,
604 >,
605 >;
606
607 fn poll_ready(
608 &mut self,
609 cx: &mut Context<'_>,
610 ) -> Poll<std::result::Result<(), Self::Error>> {
611 self.inner.poll_ready(cx).map_err(Into::into)
612 }
613
614 fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
615 let manager = self.manager.clone();
616 let keyer = self.keyer.clone();
617 let options = self.options.clone();
618 let metrics = self.metrics.clone();
619 let mut inner = self.inner.clone();
620
621 Box::pin(async move {
622 // Store request parts for later use in should_cache
623 let (req_parts, req_body) = req.into_parts();
624
625 // Generate cache key from request parts
626 let temp_req = Request::from_parts(req_parts.clone(), ());
627 let cache_key = keyer.cache_key(&temp_req);
628
629 // Try to get from cache
630 if let Ok(Some((cached_resp, policy))) =
631 manager.get(&cache_key).await
632 {
633 // Deserialize cached response first
634 if let Ok(cached) =
635 serde_json::from_slice::<CachedResponse>(&cached_resp.body)
636 {
637 // Check freshness using both CachePolicy and our TTL tracking.
638 // CachePolicy handles Vary header matching.
639 // Our is_stale() handles the TTL we assigned (especially for cache_by_default).
640 let before_req =
641 policy.before_request(&req_parts, SystemTime::now());
642
643 // Determine if response had explicit freshness directives
644 // (max-age or s-maxage). If it only has "public" or other directives
645 // without explicit TTL, we use our own TTL tracking.
646 let has_explicit_ttl = cached
647 .headers
648 .get("cache-control")
649 .is_some_and(|cc_values| {
650 cc_values.iter().any(|cc| {
651 cc.contains("max-age")
652 || cc.contains("s-maxage")
653 })
654 });
655
656 let is_fresh = match before_req {
657 BeforeRequest::Fresh(_) => {
658 // CachePolicy says fresh - use it
659 true
660 }
661 BeforeRequest::Stale { .. } => {
662 // CachePolicy says stale. This could be due to:
663 // 1. Vary header mismatch
664 // 2. Time-based staleness per cache headers
665 // 3. No explicit TTL (cache_by_default or public-only)
666 //
667 // For case 3, our TTL tracking is authoritative.
668 // For cases 1-2, we should respect CachePolicy.
669 if has_explicit_ttl {
670 // Had explicit TTL - trust CachePolicy
671 false
672 } else {
673 // No explicit TTL - use our TTL
674 !cached.is_stale()
675 }
676 }
677 };
678
679 if is_fresh {
680 // Cache hit
681 metrics.hits.fetch_add(1, Ordering::Relaxed);
682 let mut response = cached.into_response()?;
683
684 if options.cache_status_headers {
685 response.headers_mut().insert(
686 "x-cache",
687 HeaderValue::from_static("HIT"),
688 );
689 }
690
691 return Ok(response.map(ResponseBody::Cached));
692 }
693 }
694 }
695
696 // Reconstruct request for handler
697 let req = Request::from_parts(req_parts.clone(), req_body);
698
699 // Cache miss or stale - call the handler
700 metrics.misses.fetch_add(1, Ordering::Relaxed);
701 let response = inner.call(req).await.map_err(Into::into)?;
702
703 // Split response to check if we should cache
704 let (res_parts, body) = response.into_parts();
705
706 // Check if we should cache this response
707 if let Some(ttl) = should_cache(&req_parts, &res_parts, &options) {
708 // Buffer the response body
709 let body_bytes = match collect_body(body).await {
710 Ok(bytes) => bytes,
711 Err(e) => {
712 // If we can't collect the body, return an error response
713 return Err(e);
714 }
715 };
716
717 // Check size limit
718 if body_bytes.len() <= options.max_body_size {
719 metrics.stores.fetch_add(1, Ordering::Relaxed);
720 // Create cached response
721 let cached = CachedResponse {
722 status: res_parts.status.as_u16(),
723 headers: {
724 let mut map: HashMap<String, Vec<String>> =
725 HashMap::new();
726 for (k, v) in res_parts.headers.iter() {
727 if let Ok(s) = v.to_str() {
728 map.entry(k.to_string())
729 .or_default()
730 .push(s.to_string());
731 }
732 }
733 map
734 },
735 body: body_bytes.to_vec(),
736 cached_at: SystemTime::now(),
737 ttl,
738 vary: extract_vary_headers(&res_parts),
739 };
740
741 // Store in cache (fire and forget)
742 let cached_json = serde_json::to_vec(&cached)
743 .map_err(|e| Box::new(e) as BoxError)?;
744 let http_response = HttpResponse {
745 body: cached_json,
746 headers: Default::default(),
747 status: 200,
748 url: cache_key.clone().parse().unwrap_or_else(|_| {
749 "http://localhost/".parse().unwrap()
750 }),
751 version: HttpVersion::Http11,
752 metadata: None,
753 };
754
755 // Create CachePolicy from actual request/response for Vary support
756 let policy_req = Request::from_parts(req_parts.clone(), ());
757 let policy_res =
758 Response::from_parts(res_parts.clone(), ());
759 let policy = CachePolicy::new(&policy_req, &policy_res);
760
761 // Spawn cache write asynchronously
762 let manager_clone = manager.clone();
763 tokio::spawn(async move {
764 let _ = manager_clone
765 .put(cache_key, http_response, policy)
766 .await;
767 });
768 } else {
769 // Body too large
770 metrics.skipped.fetch_add(1, Ordering::Relaxed);
771 }
772
773 // Return response with MISS header
774 let mut response = Response::from_parts(res_parts, body_bytes);
775 if options.cache_status_headers {
776 response
777 .headers_mut()
778 .insert("x-cache", HeaderValue::from_static("MISS"));
779 }
780 return Ok(response.map(ResponseBody::Fresh));
781 }
782
783 // Don't cache - just return
784 metrics.skipped.fetch_add(1, Ordering::Relaxed);
785 let body_bytes = collect_body(body).await?;
786 Ok(Response::from_parts(res_parts, body_bytes)
787 .map(ResponseBody::Uncacheable))
788 })
789 }
790}
791
792/// Collect a body into bytes.
793async fn collect_body<B>(body: B) -> std::result::Result<Bytes, BoxError>
794where
795 B: HttpBody,
796 B::Error: Into<BoxError>,
797{
798 body.collect()
799 .await
800 .map(|collected| collected.to_bytes())
801 .map_err(Into::into)
802}
803
804/// Extract Vary headers from response parts.
805fn extract_vary_headers(parts: &http::response::Parts) -> Option<Vec<String>> {
806 parts
807 .headers
808 .get(http::header::VARY)
809 .and_then(|v| v.to_str().ok())
810 .map(|s| s.split(',').map(|h| h.trim().to_string()).collect())
811}
812
813/// Determine if a response should be cached based on its headers.
814/// Implements RFC 7234/9111 requirements for shared caches.
815/// Helper function to check if a Cache-Control directive is present.
816/// This properly parses directives by splitting on commas and matching exact names.
817fn has_directive(cache_control: &str, directive: &str) -> bool {
818 cache_control
819 .split(',')
820 .map(|d| d.trim())
821 .any(|d| d == directive || d.starts_with(&format!("{}=", directive)))
822}
823
824/// Check if response explicitly permits caching of authorized requests per RFC 9111 §3.5.
825///
826/// Returns true if the response contains directives that allow caching despite
827/// the request having an Authorization header.
828fn response_permits_authorized_caching(cc_str: &str) -> bool {
829 has_directive(cc_str, "public")
830 || has_directive(cc_str, "s-maxage")
831 || has_directive(cc_str, "must-revalidate")
832}
833
834fn should_cache(
835 req_parts: &http::request::Parts,
836 res_parts: &http::response::Parts,
837 options: &ServerCacheOptions,
838) -> Option<Duration> {
839 // RFC 7234: Only cache successful responses (2xx)
840 if !res_parts.status.is_success() {
841 return None;
842 }
843
844 // RFC 9111 §3.5: Check Authorization header
845 let has_authorization =
846 req_parts.headers.contains_key(http::header::AUTHORIZATION);
847
848 // RFC 7234: Check Cache-Control directives
849 if let Some(cc) = res_parts.headers.get(http::header::CACHE_CONTROL) {
850 let cc_str = cc.to_str().ok()?;
851
852 // RFC 9111 §3.5: If request has Authorization header, only cache if
853 // response explicitly permits it
854 if has_authorization
855 && options.respect_authorization
856 && !response_permits_authorized_caching(cc_str)
857 {
858 return None;
859 }
860
861 // RFC 7234: MUST NOT store if no-store directive present
862 if has_directive(cc_str, "no-store") {
863 return None;
864 }
865
866 // RFC 7234: MUST NOT store if no-cache
867 // Note: Per RFC, no-cache means "cache but always revalidate". However,
868 // without conditional request support (ETag/If-None-Match), we cannot
869 // revalidate, so we skip caching entirely.
870 if has_directive(cc_str, "no-cache") {
871 return None;
872 }
873
874 // RFC 7234: Shared caches MUST NOT store responses with private directive
875 if has_directive(cc_str, "private") {
876 return None;
877 }
878
879 // RFC 7234: s-maxage directive overrides max-age for shared caches
880 if let Some(s_maxage) = parse_s_maxage(cc_str) {
881 let ttl = Duration::from_secs(s_maxage);
882 let ttl = apply_ttl_constraints(ttl, options);
883 return Some(ttl);
884 }
885
886 // RFC 7234: Extract max-age for cache lifetime
887 if let Some(max_age) = parse_max_age(cc_str) {
888 let ttl = Duration::from_secs(max_age);
889 let ttl = apply_ttl_constraints(ttl, options);
890 return Some(ttl);
891 }
892
893 // RFC 7234: public directive makes response cacheable
894 if has_directive(cc_str, "public") {
895 return options.default_ttl;
896 }
897 } else {
898 // No Cache-Control header
899 // RFC 9111 §3.5: Don't cache authorized requests without explicit permission
900 if has_authorization && options.respect_authorization {
901 return None;
902 }
903 }
904
905 // RFC 7234: Check for Expires header if no Cache-Control
906 if let Some(expires) = res_parts.headers.get(http::header::EXPIRES) {
907 if let Ok(expires_str) = expires.to_str() {
908 if let Some(ttl) = parse_expires(expires_str) {
909 let ttl = apply_ttl_constraints(ttl, options);
910 return Some(ttl);
911 }
912 }
913 }
914
915 // No explicit caching directive
916 if options.cache_by_default {
917 options.default_ttl
918 } else {
919 None
920 }
921}
922
923/// Apply min/max TTL constraints from options.
924fn apply_ttl_constraints(
925 ttl: Duration,
926 options: &ServerCacheOptions,
927) -> Duration {
928 let mut result = ttl;
929
930 if let Some(max) = options.max_ttl {
931 result = result.min(max);
932 }
933
934 if let Some(min) = options.min_ttl {
935 result = result.max(min);
936 }
937
938 result
939}
940
941/// Parse max-age from Cache-Control header.
942fn parse_max_age(cache_control: &str) -> Option<u64> {
943 for directive in cache_control.split(',') {
944 let directive = directive.trim();
945 if let Some(value) = directive.strip_prefix("max-age=") {
946 return value.parse().ok();
947 }
948 }
949 None
950}
951
952/// Parse s-maxage from Cache-Control header (shared cache specific).
953fn parse_s_maxage(cache_control: &str) -> Option<u64> {
954 for directive in cache_control.split(',') {
955 let directive = directive.trim();
956 if let Some(value) = directive.strip_prefix("s-maxage=") {
957 return value.parse().ok();
958 }
959 }
960 None
961}
962
963/// Parse Expires header to calculate TTL.
964///
965/// Returns the duration until expiration, or None if the date is invalid or in the past.
966fn parse_expires(expires: &str) -> Option<Duration> {
967 let expires_time = httpdate::parse_http_date(expires).ok()?;
968 let now = SystemTime::now();
969
970 expires_time.duration_since(now).ok()
971}
972
973#[cfg(test)]
974mod tests {
975 use super::*;
976
977 #[test]
978 fn test_default_keyer() {
979 let keyer = DefaultKeyer;
980 let req = Request::get("/users/123").body(()).unwrap();
981 let key = keyer.cache_key(&req);
982 assert_eq!(key, "GET /users/123");
983 }
984
985 #[test]
986 fn test_query_keyer() {
987 let keyer = QueryKeyer;
988 let req = Request::get("/users?page=1").body(()).unwrap();
989 let key = keyer.cache_key(&req);
990 assert_eq!(key, "GET /users?page=1");
991 }
992
993 #[test]
994 fn test_parse_max_age() {
995 assert_eq!(parse_max_age("max-age=3600"), Some(3600));
996 assert_eq!(parse_max_age("public, max-age=3600"), Some(3600));
997 assert_eq!(parse_max_age("max-age=3600, public"), Some(3600));
998 assert_eq!(parse_max_age("public"), None);
999 }
1000
1001 #[test]
1002 fn test_parse_s_maxage() {
1003 assert_eq!(parse_s_maxage("s-maxage=7200"), Some(7200));
1004 assert_eq!(parse_s_maxage("public, s-maxage=7200"), Some(7200));
1005 assert_eq!(parse_s_maxage("s-maxage=7200, max-age=3600"), Some(7200));
1006 assert_eq!(parse_s_maxage("public"), None);
1007 }
1008
1009 #[test]
1010 fn test_apply_ttl_constraints() {
1011 let options = ServerCacheOptions {
1012 min_ttl: Some(Duration::from_secs(10)),
1013 max_ttl: Some(Duration::from_secs(100)),
1014 ..Default::default()
1015 };
1016
1017 assert_eq!(
1018 apply_ttl_constraints(Duration::from_secs(5), &options),
1019 Duration::from_secs(10)
1020 );
1021 assert_eq!(
1022 apply_ttl_constraints(Duration::from_secs(50), &options),
1023 Duration::from_secs(50)
1024 );
1025 assert_eq!(
1026 apply_ttl_constraints(Duration::from_secs(200), &options),
1027 Duration::from_secs(100)
1028 );
1029 }
1030}