Skip to main content

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}