http_cache/lib.rs
1#![forbid(unsafe_code, future_incompatible)]
2#![deny(
3 missing_docs,
4 missing_debug_implementations,
5 missing_copy_implementations,
6 nonstandard_style,
7 unused_qualifications,
8 unused_import_braces,
9 unused_extern_crates,
10 trivial_casts,
11 trivial_numeric_casts
12)]
13#![allow(clippy::doc_lazy_continuation)]
14#![cfg_attr(docsrs, feature(doc_cfg))]
15//! A caching middleware that follows HTTP caching rules, thanks to
16//! [`http-cache-semantics`](https://github.com/kornelski/rusty-http-cache-semantics).
17//! By default, it uses [`cacache`](https://github.com/zkat/cacache-rs) as the backend cache manager.
18//!
19//! This crate provides the core HTTP caching functionality that can be used to build
20//! caching middleware for various HTTP clients and server frameworks. It implements
21//! RFC 7234 HTTP caching semantics, supporting features like:
22//!
23//! - Automatic cache invalidation for unsafe HTTP methods (PUT, POST, DELETE, PATCH)
24//! - Respect for HTTP cache-control headers
25//! - Conditional requests (ETag, Last-Modified)
26//! - Multiple cache storage backends
27//! - Streaming response support
28//!
29//! ## Basic Usage
30//!
31//! The core types for building HTTP caches:
32//!
33//! ```rust
34//! # #[cfg(feature = "manager-cacache")]
35//! # fn main() {
36//! use http_cache::{CACacheManager, HttpCache, CacheMode, HttpCacheOptions};
37//!
38//! // Create a cache manager with disk storage
39//! let manager = CACacheManager::new("./cache".into(), true);
40//!
41//! // Create an HTTP cache with default behavior
42//! let cache = HttpCache {
43//! mode: CacheMode::Default,
44//! manager,
45//! options: HttpCacheOptions::default(),
46//! };
47//! # }
48//! # #[cfg(not(feature = "manager-cacache"))]
49//! # fn main() {}
50//! ```
51//!
52//! ## Cache Modes
53//!
54//! Different cache modes provide different behaviors:
55//!
56//! ```rust
57//! # #[cfg(feature = "manager-cacache")]
58//! # fn main() {
59//! use http_cache::{CacheMode, HttpCache, CACacheManager, HttpCacheOptions};
60//!
61//! let manager = CACacheManager::new("./cache".into(), true);
62//!
63//! // Default mode: follows HTTP caching rules
64//! let default_cache = HttpCache {
65//! mode: CacheMode::Default,
66//! manager: manager.clone(),
67//! options: HttpCacheOptions::default(),
68//! };
69//!
70//! // NoStore mode: never caches responses
71//! let no_store_cache = HttpCache {
72//! mode: CacheMode::NoStore,
73//! manager: manager.clone(),
74//! options: HttpCacheOptions::default(),
75//! };
76//!
77//! // ForceCache mode: caches responses even if headers suggest otherwise
78//! let force_cache = HttpCache {
79//! mode: CacheMode::ForceCache,
80//! manager,
81//! options: HttpCacheOptions::default(),
82//! };
83//! # }
84//! # #[cfg(not(feature = "manager-cacache"))]
85//! # fn main() {}
86//! ```
87//!
88//! ## Custom Cache Keys
89//!
90//! You can customize how cache keys are generated:
91//!
92//! ```rust
93//! # #[cfg(feature = "manager-cacache")]
94//! # fn main() {
95//! use http_cache::{HttpCacheOptions, CACacheManager, HttpCache, CacheMode};
96//! use std::sync::Arc;
97//! use http::request::Parts;
98//!
99//! let manager = CACacheManager::new("./cache".into(), true);
100//!
101//! let options = HttpCacheOptions {
102//! cache_key: Some(Arc::new(|req: &Parts| {
103//! // Custom cache key that includes query parameters
104//! format!("{}:{}", req.method, req.uri)
105//! })),
106//! ..Default::default()
107//! };
108//!
109//! let cache = HttpCache {
110//! mode: CacheMode::Default,
111//! manager,
112//! options,
113//! };
114//! # }
115//! # #[cfg(not(feature = "manager-cacache"))]
116//! # fn main() {}
117//! ```
118//!
119//! ## Maximum TTL Control
120//!
121//! Set a maximum time-to-live for cached responses, particularly useful with `CacheMode::IgnoreRules`:
122//!
123//! ```rust
124//! # #[cfg(feature = "manager-cacache")]
125//! # fn main() {
126//! use http_cache::{HttpCacheOptions, CACacheManager, HttpCache, CacheMode};
127//! use std::time::Duration;
128//!
129//! let manager = CACacheManager::new("./cache".into(), true);
130//!
131//! // Limit cache duration to 5 minutes regardless of server headers
132//! let options = HttpCacheOptions {
133//! max_ttl: Some(Duration::from_secs(300)), // 5 minutes
134//! ..Default::default()
135//! };
136//!
137//! let cache = HttpCache {
138//! mode: CacheMode::IgnoreRules, // Ignore server cache-control headers
139//! manager,
140//! options,
141//! };
142//! # }
143//! # #[cfg(not(feature = "manager-cacache"))]
144//! # fn main() {}
145//! ```
146//!
147//! ## Response-Based Cache Mode Override
148//!
149//! Override cache behavior based on the response you receive. This is useful for scenarios like
150//! forcing cache for successful responses even when headers say not to cache, or never caching
151//! error responses like rate limits:
152//!
153//! ```rust
154//! # #[cfg(feature = "manager-cacache")]
155//! # fn main() {
156//! use http_cache::{HttpCacheOptions, CACacheManager, HttpCache, CacheMode};
157//! use std::sync::Arc;
158//!
159//! let manager = CACacheManager::new("./cache".into(), true);
160//!
161//! let options = HttpCacheOptions {
162//! response_cache_mode_fn: Some(Arc::new(|_request_parts, response| {
163//! match response.status {
164//! // Force cache successful responses even if headers say not to cache
165//! 200..=299 => Some(CacheMode::ForceCache),
166//! // Never cache rate-limited responses
167//! 429 => Some(CacheMode::NoStore),
168//! // Use default behavior for everything else
169//! _ => None,
170//! }
171//! })),
172//! ..Default::default()
173//! };
174//!
175//! let cache = HttpCache {
176//! mode: CacheMode::Default,
177//! manager,
178//! options,
179//! };
180//! # }
181//! # #[cfg(not(feature = "manager-cacache"))]
182//! # fn main() {}
183//! ```
184//!
185//! ## Content-Type Based Caching
186//!
187//! You can implement selective caching based on response content types using `response_cache_mode_fn`.
188//! This is useful when you only want to cache certain types of content:
189//!
190//! ```rust
191//! # #[cfg(feature = "manager-cacache")]
192//! # fn main() {
193//! use http_cache::{HttpCacheOptions, CACacheManager, HttpCache, CacheMode};
194//! use std::sync::Arc;
195//!
196//! let manager = CACacheManager::new("./cache".into(), true);
197//!
198//! let options = HttpCacheOptions {
199//! response_cache_mode_fn: Some(Arc::new(|_request_parts, response| {
200//! // Check the Content-Type header to decide caching behavior
201//! if let Some(content_type) = response.headers.get("content-type") {
202//! match content_type.as_str() {
203//! // Cache JSON APIs aggressively
204//! ct if ct.starts_with("application/json") => Some(CacheMode::ForceCache),
205//! // Cache images with default rules
206//! ct if ct.starts_with("image/") => Some(CacheMode::Default),
207//! // Cache static assets
208//! ct if ct.starts_with("text/css") => Some(CacheMode::ForceCache),
209//! ct if ct.starts_with("application/javascript") => Some(CacheMode::ForceCache),
210//! // Don't cache HTML pages (dynamic content)
211//! ct if ct.starts_with("text/html") => Some(CacheMode::NoStore),
212//! // Don't cache unknown content types
213//! _ => Some(CacheMode::NoStore),
214//! }
215//! } else {
216//! // No Content-Type header - don't cache
217//! Some(CacheMode::NoStore)
218//! }
219//! })),
220//! ..Default::default()
221//! };
222//!
223//! let cache = HttpCache {
224//! mode: CacheMode::Default, // This gets overridden by response_cache_mode_fn
225//! manager,
226//! options,
227//! };
228//! # }
229//! # #[cfg(not(feature = "manager-cacache"))]
230//! # fn main() {}
231//! ```
232//!
233//! ## Streaming Support
234//!
235//! For handling large responses without full buffering, use the `StreamingManager`:
236//!
237//! ```rust
238//! # #[cfg(feature = "streaming")]
239//! # {
240//! use http_cache::StreamingBody;
241//! use bytes::Bytes;
242//! use http_body::Body;
243//! use http_body_util::Full;
244//!
245//! // StreamingManager uses redb (metadata) + tokio::fs (bodies) with a moka
246//! // in-memory hot cache for disk-backed streaming.
247//! // Create with: StreamingManager::with_temp_dir(1000).await.unwrap()
248//!
249//! // StreamingBody can handle both buffered and streaming scenarios
250//! let body: StreamingBody<Full<Bytes>> = StreamingBody::buffered(Bytes::from("cached content"));
251//! println!("Body size: {:?}", body.size_hint());
252//! # }
253//! ```
254//!
255//! **Note**: Streaming support requires the `StreamingManager` with the `streaming` feature.
256//! Other cache managers (CACacheManager, MokaManager, QuickManager) do not support streaming
257//! and will buffer response bodies in memory.
258//!
259//! ## Features
260//!
261//! The following features are available. By default `manager-cacache` is enabled.
262//!
263//! - `manager-cacache` (default): enable [cacache](https://github.com/zkat/cacache-rs),
264//! a disk cache, backend manager. Uses tokio runtime.
265//! - `manager-moka` (disabled): enable [moka](https://github.com/moka-rs/moka),
266//! an in-memory cache, backend manager.
267//! - `manager-foyer` (disabled): enable [foyer](https://github.com/foyer-rs/foyer),
268//! a hybrid in-memory + disk cache, backend manager. Uses tokio runtime.
269//! - `http-headers-compat` (disabled): enable backwards compatibility for deserializing cached
270//! responses from older versions that used single-value headers. Enable this if you need to read
271//! cache entries created by older versions of http-cache.
272//! - `streaming` (disabled): enable the `StreamingManager` for streaming cache
273//! support. Uses redb for metadata, raw `tokio::fs` files for bodies, and
274//! moka as an in-memory hot cache of metadata.
275//! - `with-http-types` (disabled): enable [http-types](https://github.com/http-rs/http-types)
276//! type conversion support
277//!
278//! ### URL Implementation Features
279//!
280//! Exactly one URL implementation must be enabled. These features are **mutually exclusive**:
281//!
282//! - `url-standard` (default): uses the [url](https://github.com/servo/rust-url) crate.
283//! Note: This brings in the `idna` crate which has a Unicode license.
284//! - `url-ada` (disabled): uses [ada-url](https://github.com/ada-url/rust) for WHATWG-compliant
285//! URL parsing without the Unicode/IDNA license dependency.
286//!
287//! If you need to avoid the Unicode license, use `url-ada`:
288//!
289//! ```toml
290//! [dependencies]
291//! http-cache = { version = "1.0", default-features = false, features = ["manager-cacache", "url-ada"] }
292//! ```
293//!
294//! ### Legacy bincode features (deprecated)
295//!
296//! These features are deprecated due to [RUSTSEC-2025-0141](https://rustsec.org/advisories/RUSTSEC-2025-0141)
297//! and will be removed in the next major version:
298//!
299//! - `manager-cacache-bincode`: cacache with bincode serialization
300//! - `manager-moka-bincode`: moka with bincode serialization
301//!
302//! **Note**: Only `StreamingManager` (via the `streaming` feature) provides streaming support.
303//! Other managers will buffer response bodies in memory even when used with `StreamingManager`.
304//!
305//! ## Integration
306//!
307//! This crate is designed to be used as a foundation for HTTP client and server middleware.
308//! See the companion crates for specific integrations:
309//!
310//! - [`http-cache-reqwest`](https://docs.rs/http-cache-reqwest) for reqwest client middleware
311//! - [`http-cache-surf`](https://docs.rs/http-cache-surf) for surf client middleware
312//! - [`http-cache-tower`](https://docs.rs/http-cache-tower) for tower service middleware
313
314// URL feature validation - exactly one URL implementation must be enabled
315#[cfg(all(feature = "url-standard", feature = "url-ada"))]
316compile_error!("features `url-standard` and `url-ada` are mutually exclusive");
317
318#[cfg(not(any(feature = "url-standard", feature = "url-ada")))]
319compile_error!("either feature `url-standard` or `url-ada` must be enabled");
320
321mod body;
322mod error;
323mod managers;
324
325#[cfg(feature = "rate-limiting")]
326pub mod rate_limiting;
327
328use std::{
329 collections::HashMap,
330 convert::TryFrom,
331 fmt::{self, Debug},
332 future::Future,
333 str::FromStr,
334 sync::Arc,
335 time::{Duration, SystemTime},
336};
337
338use http::{
339 header::CACHE_CONTROL, request, response, HeaderValue, Response, StatusCode,
340};
341use http_cache_semantics::{AfterResponse, BeforeRequest, CachePolicy};
342use serde::{Deserialize, Deserializer, Serialize, Serializer};
343
344// URL type alias - allows users to choose between `url` (default) and `ada-url` crates
345// When using `url-ada` feature, this becomes `ada_url::Url`
346#[cfg(feature = "url-ada")]
347pub use ada_url::Url;
348#[cfg(not(feature = "url-ada"))]
349pub use url::Url;
350
351// ============================================================================
352// URL Helper Functions
353// ============================================================================
354// These functions abstract away API differences between `url` and `ada-url` crates.
355// Internal code should use these helpers instead of calling URL methods directly.
356
357/// Parse a URL string into a `Url` type.
358///
359/// This helper abstracts the parsing API difference between `url` and `ada-url`:
360/// - `url` crate: `Url::parse(s)` returns `Result<Url, ParseError>`
361/// - `ada-url` crate: `Url::parse(s, None)` returns `Result<Url, ParseUrlError>`
362#[inline]
363pub fn url_parse(s: &str) -> Result<Url> {
364 #[cfg(feature = "url-ada")]
365 {
366 Url::parse(s, None).map_err(|e| -> BoxError { e.to_string().into() })
367 }
368 #[cfg(not(feature = "url-ada"))]
369 {
370 Url::parse(s).map_err(|e| -> BoxError { Box::new(e) })
371 }
372}
373
374/// Set the path component of a URL.
375///
376/// API differences:
377/// - `url` crate: `url.set_path(path)`
378/// - `ada-url` crate: `url.set_pathname(Some(path))`
379#[inline]
380pub fn url_set_path(url: &mut Url, path: &str) {
381 #[cfg(feature = "url-ada")]
382 {
383 let _ = url.set_pathname(Some(path));
384 }
385 #[cfg(not(feature = "url-ada"))]
386 {
387 url.set_path(path);
388 }
389}
390
391/// Set the query component of a URL.
392///
393/// API differences:
394/// - `url` crate: `url.set_query(Some(query))` or `url.set_query(None)`
395/// - `ada-url` crate: `url.set_search(Some(query))` or `url.set_search(None)`
396#[inline]
397pub fn url_set_query(url: &mut Url, query: Option<&str>) {
398 #[cfg(feature = "url-ada")]
399 {
400 url.set_search(query);
401 }
402 #[cfg(not(feature = "url-ada"))]
403 {
404 url.set_query(query);
405 }
406}
407
408/// Get the hostname of a URL as a string.
409///
410/// API differences:
411/// - `url` crate: `url.host_str()` returns `Option<&str>`
412/// - `ada-url` crate: `url.hostname()` returns `&str` (empty string if no host)
413#[inline]
414#[must_use]
415pub fn url_hostname(url: &Url) -> Option<&str> {
416 #[cfg(feature = "url-ada")]
417 {
418 let hostname = url.hostname();
419 if hostname.is_empty() {
420 None
421 } else {
422 Some(hostname)
423 }
424 }
425 #[cfg(not(feature = "url-ada"))]
426 {
427 url.host_str()
428 }
429}
430
431/// Get the host of a URL as a string for display purposes (e.g., warning headers).
432///
433/// This returns the host portion as a string, or "unknown" if not available.
434/// Used in places like HTTP Warning headers where we need a displayable host value.
435#[inline]
436#[must_use]
437pub fn url_host_str(url: &Url) -> String {
438 #[cfg(feature = "url-ada")]
439 {
440 let hostname = url.hostname();
441 if hostname.is_empty() {
442 "unknown".to_string()
443 } else {
444 hostname.to_string()
445 }
446 }
447 #[cfg(not(feature = "url-ada"))]
448 {
449 url.host()
450 .map(|h| h.to_string())
451 .unwrap_or_else(|| "unknown".to_string())
452 }
453}
454
455pub use body::StreamingBody;
456pub use error::{
457 BadHeader, BadRequest, BadVersion, BoxError, ClientStreamingError,
458 HttpCacheError, HttpCacheResult, Result, StreamingError,
459};
460
461#[cfg(any(
462 feature = "manager-cacache",
463 feature = "manager-cacache-bincode"
464))]
465pub use managers::cacache::CACacheManager;
466
467#[cfg(feature = "streaming")]
468pub use managers::streaming_cache::StreamingManager;
469
470#[cfg(any(feature = "manager-moka", feature = "manager-moka-bincode"))]
471pub use managers::moka::MokaManager;
472
473#[cfg(feature = "manager-foyer")]
474pub use managers::foyer::FoyerManager;
475
476#[cfg(feature = "rate-limiting")]
477pub use rate_limiting::{
478 CacheAwareRateLimiter, DirectRateLimiter, DomainRateLimiter,
479};
480
481#[cfg(feature = "rate-limiting")]
482pub use rate_limiting::Quota;
483
484// Exposing the moka cache for convenience, renaming to avoid naming conflicts
485#[cfg(any(feature = "manager-moka", feature = "manager-moka-bincode"))]
486#[cfg_attr(docsrs, doc(cfg(feature = "manager-moka")))]
487pub use moka::future::{Cache as MokaCache, CacheBuilder as MokaCacheBuilder};
488
489// Custom headers used to indicate cache status (hit or miss)
490/// `x-cache` header: Value will be HIT if the response was served from cache, MISS if not
491pub const XCACHE: &str = "x-cache";
492/// `x-cache-lookup` header: Value will be HIT if a response existed in cache, MISS if not
493pub const XCACHELOOKUP: &str = "x-cache-lookup";
494/// `warning` header: HTTP warning header as per RFC 7234
495const WARNING: &str = "warning";
496
497/// Represents a basic cache status
498/// Used in the custom headers `x-cache` and `x-cache-lookup`
499#[derive(Debug, Copy, Clone)]
500pub enum HitOrMiss {
501 /// Yes, there was a hit
502 HIT,
503 /// No, there was no hit
504 MISS,
505}
506
507impl fmt::Display for HitOrMiss {
508 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
509 match self {
510 Self::HIT => write!(f, "HIT"),
511 Self::MISS => write!(f, "MISS"),
512 }
513 }
514}
515
516/// Represents an HTTP version
517#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)]
518#[non_exhaustive]
519pub enum HttpVersion {
520 /// HTTP Version 0.9
521 #[serde(rename = "HTTP/0.9")]
522 Http09,
523 /// HTTP Version 1.0
524 #[serde(rename = "HTTP/1.0")]
525 Http10,
526 /// HTTP Version 1.1
527 #[serde(rename = "HTTP/1.1")]
528 Http11,
529 /// HTTP Version 2.0
530 #[serde(rename = "HTTP/2.0")]
531 H2,
532 /// HTTP Version 3.0
533 #[serde(rename = "HTTP/3.0")]
534 H3,
535}
536
537impl fmt::Display for HttpVersion {
538 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
539 match *self {
540 HttpVersion::Http09 => write!(f, "HTTP/0.9"),
541 HttpVersion::Http10 => write!(f, "HTTP/1.0"),
542 HttpVersion::Http11 => write!(f, "HTTP/1.1"),
543 HttpVersion::H2 => write!(f, "HTTP/2.0"),
544 HttpVersion::H3 => write!(f, "HTTP/3.0"),
545 }
546 }
547}
548
549/// Extract a URL from HTTP request parts for cache key generation
550///
551/// This function reconstructs the full URL from the request parts, handling both
552/// HTTP and HTTPS schemes based on the connection type or explicit headers.
553fn extract_url_from_request_parts(parts: &request::Parts) -> Result<Url> {
554 // First check if the URI is already absolute
555 if let Some(_scheme) = parts.uri.scheme() {
556 // URI is absolute, use it directly
557 return url_parse(&parts.uri.to_string())
558 .map_err(|_| -> BoxError { BadHeader.into() });
559 }
560
561 // Get the host header
562 let host = parts
563 .headers
564 .get("host")
565 .ok_or(BadHeader)?
566 .to_str()
567 .map_err(|_| BadHeader)?;
568
569 // Determine scheme based on host and headers
570 let scheme = determine_scheme(host, &parts.headers)?;
571
572 // Create base URL using the URL helper for cross-crate compatibility
573 let mut base_url = url_parse(&format!("{}://{}/", &scheme, host))
574 .map_err(|_| -> BoxError { BadHeader.into() })?;
575
576 // Set the path and query from the URI using helpers
577 if let Some(path_and_query) = parts.uri.path_and_query() {
578 url_set_path(&mut base_url, path_and_query.path());
579 if let Some(query) = path_and_query.query() {
580 url_set_query(&mut base_url, Some(query));
581 }
582 }
583
584 Ok(base_url)
585}
586
587/// Determine the appropriate scheme for URL construction
588fn determine_scheme(host: &str, headers: &http::HeaderMap) -> Result<String> {
589 // Check for explicit protocol forwarding header first
590 if let Some(forwarded_proto) = headers.get("x-forwarded-proto") {
591 let proto = forwarded_proto.to_str().map_err(|_| BadHeader)?;
592 return match proto {
593 "http" | "https" => Ok(proto.to_string()),
594 _ => Ok("https".to_string()), // Default to secure for unknown protocols
595 };
596 }
597
598 // Check if this looks like a local development host
599 if host.starts_with("localhost") || host.starts_with("127.0.0.1") {
600 Ok("http".to_string())
601 } else {
602 Ok("https".to_string()) // Default to secure for all other hosts
603 }
604}
605
606/// Represents HTTP headers in either legacy or modern format
607#[derive(Debug, Clone)]
608pub enum HttpHeaders {
609 /// Modern header representation - allows multiple values per key
610 Modern(HashMap<String, Vec<String>>),
611 /// Legacy header representation - kept for backward compatibility with deserialization
612 #[cfg(feature = "http-headers-compat")]
613 Legacy(HashMap<String, String>),
614}
615
616// Serialize directly as the inner HashMap (no enum variant wrapper)
617// This ensures compatibility: serialized data is just the raw HashMap
618impl Serialize for HttpHeaders {
619 fn serialize<S>(
620 &self,
621 serializer: S,
622 ) -> std::result::Result<S::Ok, S::Error>
623 where
624 S: Serializer,
625 {
626 #[cfg(feature = "http-headers-compat")]
627 {
628 // Always serialize as Legacy format when compat is enabled
629 match self {
630 HttpHeaders::Modern(modern) => {
631 // Convert Modern to Legacy format by joining values
632 let legacy: HashMap<String, String> = modern
633 .iter()
634 .map(|(k, v)| (k.clone(), v.join(", ")))
635 .collect();
636 legacy.serialize(serializer)
637 }
638 HttpHeaders::Legacy(legacy) => legacy.serialize(serializer),
639 }
640 }
641
642 #[cfg(not(feature = "http-headers-compat"))]
643 {
644 match self {
645 HttpHeaders::Modern(modern) => modern.serialize(serializer),
646 }
647 }
648 }
649}
650
651// Deserialize directly as HashMap based on feature flag
652// With http-headers-compat: reads HashMap<String, String> (legacy alpha.2 format)
653// Without http-headers-compat: reads HashMap<String, Vec<String>> (modern format)
654impl<'de> Deserialize<'de> for HttpHeaders {
655 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
656 where
657 D: Deserializer<'de>,
658 {
659 #[cfg(feature = "http-headers-compat")]
660 {
661 let legacy = HashMap::<String, String>::deserialize(deserializer)?;
662 Ok(HttpHeaders::Legacy(legacy))
663 }
664
665 #[cfg(not(feature = "http-headers-compat"))]
666 {
667 let modern =
668 HashMap::<String, Vec<String>>::deserialize(deserializer)?;
669 Ok(HttpHeaders::Modern(modern))
670 }
671 }
672}
673
674impl HttpHeaders {
675 /// Creates a new empty HttpHeaders in modern format
676 pub fn new() -> Self {
677 HttpHeaders::Modern(HashMap::new())
678 }
679
680 /// Inserts a header key-value pair, replacing any existing values for that key
681 /// Keys are normalized to lowercase per RFC 7230
682 pub fn insert(&mut self, key: String, value: String) {
683 let normalized_key = key.to_ascii_lowercase();
684 match self {
685 #[cfg(feature = "http-headers-compat")]
686 HttpHeaders::Legacy(legacy) => {
687 legacy.insert(normalized_key, value);
688 }
689 HttpHeaders::Modern(modern) => {
690 // Replace existing values with a new single-element vec
691 modern.insert(normalized_key, vec![value]);
692 }
693 }
694 }
695
696 /// Appends a header value, preserving existing values for the same key
697 /// Keys are normalized to lowercase per RFC 7230
698 pub fn append(&mut self, key: String, value: String) {
699 let normalized_key = key.to_ascii_lowercase();
700 match self {
701 #[cfg(feature = "http-headers-compat")]
702 HttpHeaders::Legacy(legacy) => {
703 // Legacy format doesn't support multi-value, fall back to insert
704 legacy.insert(normalized_key, value);
705 }
706 HttpHeaders::Modern(modern) => {
707 modern
708 .entry(normalized_key)
709 .or_insert_with(Vec::new)
710 .push(value);
711 }
712 }
713 }
714
715 /// Retrieves the first value for a given header key
716 /// Keys are normalized to lowercase per RFC 7230
717 pub fn get(&self, key: &str) -> Option<&String> {
718 let normalized_key = key.to_ascii_lowercase();
719 match self {
720 #[cfg(feature = "http-headers-compat")]
721 HttpHeaders::Legacy(legacy) => legacy.get(&normalized_key),
722 HttpHeaders::Modern(modern) => {
723 modern.get(&normalized_key).and_then(|vals| vals.first())
724 }
725 }
726 }
727
728 /// Removes a header key and its associated values
729 /// Keys are normalized to lowercase per RFC 7230
730 pub fn remove(&mut self, key: &str) {
731 let normalized_key = key.to_ascii_lowercase();
732 match self {
733 #[cfg(feature = "http-headers-compat")]
734 HttpHeaders::Legacy(legacy) => {
735 legacy.remove(&normalized_key);
736 }
737 HttpHeaders::Modern(modern) => {
738 modern.remove(&normalized_key);
739 }
740 }
741 }
742
743 /// Checks if a header key exists
744 /// Keys are normalized to lowercase per RFC 7230
745 pub fn contains_key(&self, key: &str) -> bool {
746 let normalized_key = key.to_ascii_lowercase();
747 match self {
748 #[cfg(feature = "http-headers-compat")]
749 HttpHeaders::Legacy(legacy) => legacy.contains_key(&normalized_key),
750 HttpHeaders::Modern(modern) => modern.contains_key(&normalized_key),
751 }
752 }
753
754 /// Returns an iterator over the header key-value pairs
755 pub fn iter(&self) -> HttpHeadersIterator<'_> {
756 match self {
757 #[cfg(feature = "http-headers-compat")]
758 HttpHeaders::Legacy(legacy) => {
759 HttpHeadersIterator { inner: legacy.iter().collect(), index: 0 }
760 }
761 HttpHeaders::Modern(modern) => HttpHeadersIterator {
762 inner: modern
763 .iter()
764 .flat_map(|(k, vals)| vals.iter().map(move |v| (k, v)))
765 .collect(),
766 index: 0,
767 },
768 }
769 }
770}
771
772impl From<&http::HeaderMap> for HttpHeaders {
773 fn from(headers: &http::HeaderMap) -> Self {
774 let mut modern_headers = HashMap::new();
775
776 // headers.keys() already yields each unique name exactly once
777 for name in headers.keys() {
778 let values: Vec<String> = headers
779 .get_all(name)
780 .iter()
781 .filter_map(|v| v.to_str().ok())
782 .map(|s| s.to_string())
783 .collect();
784
785 if !values.is_empty() {
786 modern_headers.insert(name.to_string(), values);
787 }
788 }
789
790 HttpHeaders::Modern(modern_headers)
791 }
792}
793
794impl From<HttpHeaders> for HashMap<String, Vec<String>> {
795 fn from(headers: HttpHeaders) -> Self {
796 match headers {
797 #[cfg(feature = "http-headers-compat")]
798 HttpHeaders::Legacy(legacy) => {
799 legacy.into_iter().map(|(k, v)| (k, vec![v])).collect()
800 }
801 HttpHeaders::Modern(modern) => modern,
802 }
803 }
804}
805
806impl Default for HttpHeaders {
807 fn default() -> Self {
808 HttpHeaders::new()
809 }
810}
811
812impl IntoIterator for HttpHeaders {
813 type Item = (String, String);
814 type IntoIter = HttpHeadersIntoIterator;
815
816 fn into_iter(self) -> Self::IntoIter {
817 HttpHeadersIntoIterator {
818 inner: match self {
819 #[cfg(feature = "http-headers-compat")]
820 HttpHeaders::Legacy(legacy) => legacy.into_iter().collect(),
821 HttpHeaders::Modern(modern) => modern
822 .into_iter()
823 .flat_map(|(k, vals)| {
824 vals.into_iter().map(move |v| (k.clone(), v))
825 })
826 .collect(),
827 },
828 index: 0,
829 }
830 }
831}
832
833/// Iterator for HttpHeaders
834#[derive(Debug)]
835pub struct HttpHeadersIntoIterator {
836 inner: Vec<(String, String)>,
837 index: usize,
838}
839
840impl Iterator for HttpHeadersIntoIterator {
841 type Item = (String, String);
842
843 fn next(&mut self) -> Option<Self::Item> {
844 if self.index < self.inner.len() {
845 let item = self.inner[self.index].clone();
846 self.index += 1;
847 Some(item)
848 } else {
849 None
850 }
851 }
852}
853
854impl<'a> IntoIterator for &'a HttpHeaders {
855 type Item = (&'a String, &'a String);
856 type IntoIter = HttpHeadersIterator<'a>;
857
858 fn into_iter(self) -> Self::IntoIter {
859 self.iter()
860 }
861}
862
863/// Iterator for HttpHeaders references
864#[derive(Debug)]
865pub struct HttpHeadersIterator<'a> {
866 inner: Vec<(&'a String, &'a String)>,
867 index: usize,
868}
869
870impl<'a> Iterator for HttpHeadersIterator<'a> {
871 type Item = (&'a String, &'a String);
872
873 fn next(&mut self) -> Option<Self::Item> {
874 if self.index < self.inner.len() {
875 let item = self.inner[self.index];
876 self.index += 1;
877 Some(item)
878 } else {
879 None
880 }
881 }
882}
883
884/// A basic generic type that represents an HTTP response
885#[derive(Debug, Clone, Deserialize, Serialize)]
886pub struct HttpResponse {
887 /// HTTP response body
888 pub body: Vec<u8>,
889 /// HTTP response headers
890 pub headers: HttpHeaders,
891 /// HTTP response status code
892 pub status: u16,
893 /// HTTP response url
894 pub url: Url,
895 /// HTTP response version
896 pub version: HttpVersion,
897 /// Metadata
898 #[serde(default)]
899 pub metadata: Option<Vec<u8>>,
900}
901
902impl HttpResponse {
903 /// Returns `http::response::Parts`
904 pub fn parts(&self) -> Result<response::Parts> {
905 let mut converted =
906 response::Builder::new().status(self.status).body(())?;
907 {
908 let headers = converted.headers_mut();
909 for header in &self.headers {
910 headers.append(
911 http::header::HeaderName::from_str(header.0.as_str())?,
912 HeaderValue::from_str(header.1.as_str())?,
913 );
914 }
915 }
916 Ok(converted.into_parts().0)
917 }
918
919 /// Returns the status code of the warning header if present
920 #[must_use]
921 fn warning_code(&self) -> Option<usize> {
922 self.headers.get(WARNING).and_then(|hdr| {
923 hdr.as_str().chars().take(3).collect::<String>().parse().ok()
924 })
925 }
926
927 /// Adds a warning header to a response
928 fn add_warning(&mut self, url: &Url, code: usize, message: &str) {
929 // warning = "warning" ":" 1#warning-value
930 // warning-value = warn-code SP warn-agent SP warn-text [SP warn-date]
931 // warn-code = 3DIGIT
932 // warn-agent = ( host [ ":" port ] ) | pseudonym
933 // ; the name or pseudonym of the server adding
934 // ; the warning header, for use in debugging
935 // warn-text = quoted-string
936 // warn-date = <"> HTTP-date <">
937 // (https://tools.ietf.org/html/rfc2616#section-14.46)
938 let host = url_host_str(url);
939 // Escape message to prevent header injection and ensure valid HTTP format
940 let escaped_message =
941 message.replace('"', "'").replace(['\n', '\r'], " ");
942 self.headers.insert(
943 WARNING.to_string(),
944 format!(
945 "{} {} \"{}\" \"{}\"",
946 code,
947 host,
948 escaped_message,
949 httpdate::fmt_http_date(SystemTime::now())
950 ),
951 );
952 }
953
954 /// Removes a warning header from a response
955 fn remove_warning(&mut self) {
956 self.headers.remove(WARNING);
957 }
958
959 /// Update the headers from `http::response::Parts`
960 pub fn update_headers(&mut self, parts: &response::Parts) -> Result<()> {
961 // Clear keys that appear in the new headers, then append all values
962 for name in parts.headers.keys() {
963 self.headers.remove(name.as_str());
964 }
965 for (name, value) in parts.headers.iter() {
966 if let Ok(v) = value.to_str() {
967 self.headers.append(name.as_str().to_string(), v.to_string());
968 }
969 }
970 Ok(())
971 }
972
973 /// Checks if the Cache-Control header contains the must-revalidate directive
974 #[must_use]
975 fn must_revalidate(&self) -> bool {
976 self.headers.get(CACHE_CONTROL.as_str()).is_some_and(|val| {
977 val.as_str().to_lowercase().contains("must-revalidate")
978 })
979 }
980
981 /// Adds the custom `x-cache` header to the response
982 pub fn cache_status(&mut self, hit_or_miss: HitOrMiss) {
983 self.headers.insert(XCACHE.to_string(), hit_or_miss.to_string());
984 }
985
986 /// Adds the custom `x-cache-lookup` header to the response
987 pub fn cache_lookup_status(&mut self, hit_or_miss: HitOrMiss) {
988 self.headers.insert(XCACHELOOKUP.to_string(), hit_or_miss.to_string());
989 }
990}
991
992/// A trait providing methods for storing, reading, and removing cache records.
993pub trait CacheManager: Send + Sync + 'static {
994 /// Attempts to pull a cached response and related policy from cache.
995 fn get(
996 &self,
997 cache_key: &str,
998 ) -> impl Future<Output = Result<Option<(HttpResponse, CachePolicy)>>> + Send;
999 /// Attempts to cache a response and related policy.
1000 fn put(
1001 &self,
1002 cache_key: String,
1003 res: HttpResponse,
1004 policy: CachePolicy,
1005 ) -> impl Future<Output = Result<HttpResponse>> + Send;
1006 /// Attempts to remove a record from cache.
1007 fn delete(
1008 &self,
1009 cache_key: &str,
1010 ) -> impl Future<Output = Result<()>> + Send;
1011}
1012
1013/// A streaming cache manager that supports streaming request/response bodies
1014/// without buffering them in memory. This is ideal for large responses.
1015pub trait StreamingCacheManager: Send + Sync + 'static {
1016 /// The body type used by this cache manager
1017 type Body: http_body::Body + Send + 'static;
1018
1019 /// Attempts to pull a cached response and related policy from cache with streaming body.
1020 fn get(
1021 &self,
1022 cache_key: &str,
1023 ) -> impl Future<Output = Result<Option<(Response<Self::Body>, CachePolicy)>>>
1024 + Send
1025 where
1026 <Self::Body as http_body::Body>::Data: Send,
1027 <Self::Body as http_body::Body>::Error:
1028 Into<StreamingError> + Send + Sync + 'static;
1029
1030 /// Attempts to cache a response with a streaming body and related policy.
1031 fn put<B>(
1032 &self,
1033 cache_key: String,
1034 response: Response<B>,
1035 policy: CachePolicy,
1036 request_url: Url,
1037 metadata: Option<Vec<u8>>,
1038 ) -> impl Future<Output = Result<Response<Self::Body>>> + Send
1039 where
1040 B: http_body::Body + Send + 'static,
1041 B::Data: Send,
1042 B::Error: Into<StreamingError>,
1043 <Self::Body as http_body::Body>::Data: Send,
1044 <Self::Body as http_body::Body>::Error:
1045 Into<StreamingError> + Send + Sync + 'static;
1046
1047 /// Converts a generic body to the manager's body type for non-cacheable responses.
1048 /// This is called when a response should not be cached but still needs to be returned
1049 /// with the correct body type.
1050 fn convert_body<B>(
1051 &self,
1052 response: Response<B>,
1053 ) -> impl Future<Output = Result<Response<Self::Body>>> + Send
1054 where
1055 B: http_body::Body + Send + 'static,
1056 B::Data: Send,
1057 B::Error: Into<StreamingError>,
1058 <Self::Body as http_body::Body>::Data: Send,
1059 <Self::Body as http_body::Body>::Error:
1060 Into<StreamingError> + Send + Sync + 'static;
1061
1062 /// Attempts to remove a record from cache.
1063 fn delete(
1064 &self,
1065 cache_key: &str,
1066 ) -> impl Future<Output = Result<()>> + Send;
1067
1068 /// Creates an empty body of the manager's body type.
1069 /// Used for returning 504 Gateway Timeout responses on OnlyIfCached cache misses.
1070 fn empty_body(&self) -> Self::Body;
1071
1072 /// Convert the manager's body type to a reqwest-compatible bytes stream.
1073 /// This enables efficient streaming without collecting the entire body.
1074 #[cfg(feature = "streaming")]
1075 fn body_to_bytes_stream(
1076 body: Self::Body,
1077 ) -> impl futures_util::Stream<
1078 Item = std::result::Result<
1079 bytes::Bytes,
1080 Box<dyn std::error::Error + Send + Sync>,
1081 >,
1082 > + Send
1083 where
1084 <Self::Body as http_body::Body>::Data: Send,
1085 <Self::Body as http_body::Body>::Error: Send + Sync + 'static;
1086}
1087
1088/// Describes the functionality required for interfacing with HTTP client middleware
1089pub trait Middleware: Send {
1090 /// Allows the cache mode to be overridden.
1091 ///
1092 /// This overrides any cache mode set in the configuration, including cache_mode_fn.
1093 fn overridden_cache_mode(&self) -> Option<CacheMode> {
1094 None
1095 }
1096 /// Determines if the request method is either GET or HEAD
1097 fn is_method_get_head(&self) -> bool;
1098 /// Returns a new cache policy with default options
1099 fn policy(&self, response: &HttpResponse) -> Result<CachePolicy>;
1100 /// Returns a new cache policy with custom options
1101 fn policy_with_options(
1102 &self,
1103 response: &HttpResponse,
1104 options: CacheOptions,
1105 ) -> Result<CachePolicy>;
1106 /// Attempts to update the request headers with the passed `http::request::Parts`
1107 fn update_headers(&mut self, parts: &request::Parts) -> Result<()>;
1108 /// Attempts to force the "no-cache" directive on the request
1109 fn force_no_cache(&mut self) -> Result<()>;
1110 /// Attempts to construct `http::request::Parts` from the request
1111 fn parts(&self) -> Result<request::Parts>;
1112 /// Attempts to determine the requested url
1113 fn url(&self) -> Result<Url>;
1114 /// Attempts to determine the request method
1115 fn method(&self) -> Result<String>;
1116 /// Attempts to fetch an upstream resource and return an [`HttpResponse`]
1117 fn remote_fetch(
1118 &mut self,
1119 ) -> impl Future<Output = Result<HttpResponse>> + Send;
1120}
1121
1122/// An interface for HTTP caching that works with composable middleware patterns
1123/// like Tower. This trait separates the concerns of request analysis, cache lookup,
1124/// and response processing into discrete steps.
1125pub trait HttpCacheInterface<B = Vec<u8>>: Send + Sync {
1126 /// Analyze a request to determine cache behavior
1127 fn analyze_request(
1128 &self,
1129 parts: &request::Parts,
1130 mode_override: Option<CacheMode>,
1131 ) -> Result<CacheAnalysis>;
1132
1133 /// Look up a cached response for the given cache key
1134 #[allow(async_fn_in_trait)]
1135 async fn lookup_cached_response(
1136 &self,
1137 key: &str,
1138 ) -> Result<Option<(HttpResponse, CachePolicy)>>;
1139
1140 /// Process a fresh response from upstream and potentially cache it
1141 #[allow(async_fn_in_trait)]
1142 async fn process_response(
1143 &self,
1144 analysis: CacheAnalysis,
1145 response: Response<B>,
1146 metadata: Option<Vec<u8>>,
1147 ) -> Result<Response<B>>;
1148
1149 /// Update request headers for conditional requests (e.g., If-None-Match)
1150 fn prepare_conditional_request(
1151 &self,
1152 parts: &mut request::Parts,
1153 cached_response: &HttpResponse,
1154 policy: &CachePolicy,
1155 ) -> Result<()>;
1156
1157 /// Handle a 304 Not Modified response by returning the cached response
1158 #[allow(async_fn_in_trait)]
1159 async fn handle_not_modified(
1160 &self,
1161 cached_response: HttpResponse,
1162 fresh_parts: &response::Parts,
1163 ) -> Result<HttpResponse>;
1164}
1165
1166/// Streaming version of the HTTP cache interface that supports streaming request/response bodies
1167/// without buffering them in memory. This is ideal for large responses or when memory usage
1168/// is a concern.
1169pub trait HttpCacheStreamInterface: Send + Sync {
1170 /// The body type used by this cache implementation
1171 type Body: http_body::Body + Send + 'static;
1172
1173 /// Analyze a request to determine cache behavior
1174 fn analyze_request(
1175 &self,
1176 parts: &request::Parts,
1177 mode_override: Option<CacheMode>,
1178 ) -> Result<CacheAnalysis>;
1179
1180 /// Look up a cached response for the given cache key, returning a streaming body
1181 #[allow(async_fn_in_trait)]
1182 async fn lookup_cached_response(
1183 &self,
1184 key: &str,
1185 ) -> Result<Option<(Response<Self::Body>, CachePolicy)>>
1186 where
1187 <Self::Body as http_body::Body>::Data: Send,
1188 <Self::Body as http_body::Body>::Error:
1189 Into<StreamingError> + Send + Sync + 'static;
1190
1191 /// Process a fresh response from upstream and potentially cache it with streaming support
1192 #[allow(async_fn_in_trait)]
1193 async fn process_response<B>(
1194 &self,
1195 analysis: CacheAnalysis,
1196 response: Response<B>,
1197 metadata: Option<Vec<u8>>,
1198 ) -> Result<Response<Self::Body>>
1199 where
1200 B: http_body::Body + Send + 'static,
1201 B::Data: Send,
1202 B::Error: Into<StreamingError>,
1203 <Self::Body as http_body::Body>::Data: Send,
1204 <Self::Body as http_body::Body>::Error:
1205 Into<StreamingError> + Send + Sync + 'static;
1206
1207 /// Update request headers for conditional requests (e.g., If-None-Match)
1208 fn prepare_conditional_request(
1209 &self,
1210 parts: &mut request::Parts,
1211 cached_response: &Response<Self::Body>,
1212 policy: &CachePolicy,
1213 ) -> Result<()>;
1214
1215 /// Handle a 304 Not Modified response by returning the cached response
1216 #[allow(async_fn_in_trait)]
1217 async fn handle_not_modified(
1218 &self,
1219 cached_response: Response<Self::Body>,
1220 fresh_parts: &response::Parts,
1221 ) -> Result<Response<Self::Body>>
1222 where
1223 <Self::Body as http_body::Body>::Data: Send,
1224 <Self::Body as http_body::Body>::Error:
1225 Into<StreamingError> + Send + Sync + 'static;
1226}
1227
1228/// Analysis result for a request, containing cache key and caching decisions
1229#[derive(Debug, Clone)]
1230pub struct CacheAnalysis {
1231 /// The cache key for this request
1232 pub cache_key: String,
1233 /// Whether this request should be cached
1234 pub should_cache: bool,
1235 /// The effective cache mode for this request
1236 pub cache_mode: CacheMode,
1237 /// Keys to bust from cache before processing
1238 pub cache_bust_keys: Vec<String>,
1239 /// The request parts for policy creation
1240 pub request_parts: request::Parts,
1241 /// Whether this is a GET or HEAD request
1242 pub is_get_head: bool,
1243}
1244
1245/// Describes the type of fetch to perform when the streaming cache
1246/// orchestrator needs to make a network request.
1247///
1248/// The streaming `run` method accepts a callback `FnOnce(FetchRequest) -> Fut`
1249/// rather than an `impl Middleware`, so this enum tells the caller whether to
1250/// issue a fresh request or a conditional (revalidation) request.
1251#[derive(Debug)]
1252pub enum FetchRequest {
1253 /// A fresh fetch (cache miss or forced).
1254 Fresh,
1255 /// A fresh fetch where the caller should add `cache-control: no-cache`
1256 /// to the outgoing request. Used for [`CacheMode::NoCache`] to signal
1257 /// upstream caches that they must revalidate.
1258 FreshNoCache,
1259 /// A conditional fetch for revalidation. The contained
1260 /// [`request::Parts`] carry the conditional headers (e.g.
1261 /// `If-None-Match`, `If-Modified-Since`) that should be merged into
1262 /// the outgoing request before sending it. Callers should replace
1263 /// existing headers of the same name (insert, not append).
1264 Conditional(Box<request::Parts>),
1265}
1266
1267/// Cache mode determines how the HTTP cache behaves for requests.
1268///
1269/// These modes are similar to [make-fetch-happen cache options](https://github.com/npm/make-fetch-happen#--optscache)
1270/// and provide fine-grained control over caching behavior.
1271///
1272/// # Examples
1273///
1274/// ```rust
1275/// # #[cfg(feature = "manager-cacache")]
1276/// # fn main() {
1277/// use http_cache::{CacheMode, HttpCache, CACacheManager, HttpCacheOptions};
1278///
1279/// let manager = CACacheManager::new("./cache".into(), true);
1280///
1281/// // Use different cache modes for different scenarios
1282/// let default_cache = HttpCache {
1283/// mode: CacheMode::Default, // Standard HTTP caching rules
1284/// manager: manager.clone(),
1285/// options: HttpCacheOptions::default(),
1286/// };
1287///
1288/// let force_cache = HttpCache {
1289/// mode: CacheMode::ForceCache, // Cache everything, ignore staleness
1290/// manager: manager.clone(),
1291/// options: HttpCacheOptions::default(),
1292/// };
1293///
1294/// let no_cache = HttpCache {
1295/// mode: CacheMode::NoStore, // Never cache anything
1296/// manager,
1297/// options: HttpCacheOptions::default(),
1298/// };
1299/// # }
1300/// # #[cfg(not(feature = "manager-cacache"))]
1301/// # fn main() {}
1302/// ```
1303#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
1304pub enum CacheMode {
1305 /// Standard HTTP caching behavior (recommended for most use cases).
1306 ///
1307 /// This mode:
1308 /// - Checks the cache for fresh responses and uses them
1309 /// - Makes conditional requests for stale responses (revalidation)
1310 /// - Makes normal requests when no cached response exists
1311 /// - Updates the cache with new responses
1312 /// - Falls back to stale responses if revalidation fails
1313 ///
1314 /// This is the most common mode and follows HTTP caching standards closely.
1315 #[default]
1316 Default,
1317
1318 /// Completely bypasses the cache.
1319 ///
1320 /// This mode:
1321 /// - Never reads from the cache
1322 /// - Never writes to the cache
1323 /// - Always makes fresh network requests
1324 ///
1325 /// Use this when you need to ensure every request goes to the origin server.
1326 NoStore,
1327
1328 /// Bypasses cache on request but updates cache with response.
1329 ///
1330 /// This mode:
1331 /// - Ignores any cached responses
1332 /// - Always makes a fresh network request
1333 /// - Updates the cache with the response
1334 ///
1335 /// Equivalent to a "hard refresh" - useful when you know the cache is stale.
1336 Reload,
1337
1338 /// Always revalidates cached responses.
1339 ///
1340 /// This mode:
1341 /// - Makes conditional requests if a cached response exists
1342 /// - Makes normal requests if no cached response exists
1343 /// - Updates the cache with responses
1344 ///
1345 /// Use this when you want to ensure content freshness while still benefiting
1346 /// from conditional requests (304 Not Modified responses).
1347 NoCache,
1348
1349 /// Uses cached responses regardless of staleness.
1350 ///
1351 /// This mode:
1352 /// - Uses any cached response, even if stale
1353 /// - Makes network requests only when no cached response exists
1354 /// - Updates the cache with new responses
1355 ///
1356 /// Useful for offline scenarios or when performance is more important than freshness.
1357 ForceCache,
1358
1359 /// Only serves from cache, never makes network requests.
1360 ///
1361 /// This mode:
1362 /// - Uses any cached response, even if stale
1363 /// - Returns an error if no cached response exists
1364 /// - Never makes network requests
1365 ///
1366 /// Use this for offline-only scenarios or when you want to guarantee
1367 /// no network traffic.
1368 OnlyIfCached,
1369
1370 /// Ignores HTTP caching rules and caches everything.
1371 ///
1372 /// This mode:
1373 /// - Caches all 200 responses regardless of cache-control headers
1374 /// - Uses cached responses regardless of staleness
1375 /// - Makes network requests when no cached response exists
1376 ///
1377 /// Use this when you want aggressive caching and don't want to respect
1378 /// server cache directives.
1379 IgnoreRules,
1380}
1381
1382impl TryFrom<http::Version> for HttpVersion {
1383 type Error = BoxError;
1384
1385 fn try_from(value: http::Version) -> Result<Self> {
1386 Ok(match value {
1387 http::Version::HTTP_09 => Self::Http09,
1388 http::Version::HTTP_10 => Self::Http10,
1389 http::Version::HTTP_11 => Self::Http11,
1390 http::Version::HTTP_2 => Self::H2,
1391 http::Version::HTTP_3 => Self::H3,
1392 _ => return Err(Box::new(BadVersion)),
1393 })
1394 }
1395}
1396
1397impl From<HttpVersion> for http::Version {
1398 fn from(value: HttpVersion) -> Self {
1399 match value {
1400 HttpVersion::Http09 => Self::HTTP_09,
1401 HttpVersion::Http10 => Self::HTTP_10,
1402 HttpVersion::Http11 => Self::HTTP_11,
1403 HttpVersion::H2 => Self::HTTP_2,
1404 HttpVersion::H3 => Self::HTTP_3,
1405 }
1406 }
1407}
1408
1409#[cfg(feature = "http-types")]
1410impl TryFrom<http_types::Version> for HttpVersion {
1411 type Error = BoxError;
1412
1413 fn try_from(value: http_types::Version) -> Result<Self> {
1414 Ok(match value {
1415 http_types::Version::Http0_9 => Self::Http09,
1416 http_types::Version::Http1_0 => Self::Http10,
1417 http_types::Version::Http1_1 => Self::Http11,
1418 http_types::Version::Http2_0 => Self::H2,
1419 http_types::Version::Http3_0 => Self::H3,
1420 _ => return Err(Box::new(BadVersion)),
1421 })
1422 }
1423}
1424
1425#[cfg(feature = "http-types")]
1426impl From<HttpVersion> for http_types::Version {
1427 fn from(value: HttpVersion) -> Self {
1428 match value {
1429 HttpVersion::Http09 => Self::Http0_9,
1430 HttpVersion::Http10 => Self::Http1_0,
1431 HttpVersion::Http11 => Self::Http1_1,
1432 HttpVersion::H2 => Self::Http2_0,
1433 HttpVersion::H3 => Self::Http3_0,
1434 }
1435 }
1436}
1437
1438/// Options struct provided by
1439/// [`http-cache-semantics`](https://github.com/kornelski/rusty-http-cache-semantics).
1440pub use http_cache_semantics::CacheOptions;
1441
1442/// A closure that takes [`http::request::Parts`] and returns a [`String`].
1443/// By default, the cache key is a combination of the request method and uri with a colon in between.
1444pub type CacheKey = Arc<dyn Fn(&request::Parts) -> String + Send + Sync>;
1445
1446/// A closure that takes [`http::request::Parts`] and returns a [`CacheMode`]
1447pub type CacheModeFn = Arc<dyn Fn(&request::Parts) -> CacheMode + Send + Sync>;
1448
1449/// A closure that takes [`http::request::Parts`], [`HttpResponse`] and returns a [`CacheMode`] to override caching behavior based on the response
1450pub type ResponseCacheModeFn = Arc<
1451 dyn Fn(&request::Parts, &HttpResponse) -> Option<CacheMode> + Send + Sync,
1452>;
1453
1454/// A closure that takes [`http::request::Parts`], [`Option<CacheKey>`], the default cache key ([`&str`]) and returns [`Vec<String>`] of keys to bust the cache for.
1455/// An empty vector means that no cache busting will be performed.
1456pub type CacheBust = Arc<
1457 dyn Fn(&request::Parts, &Option<CacheKey>, &str) -> Vec<String>
1458 + Send
1459 + Sync,
1460>;
1461
1462/// Type alias for metadata stored alongside cached responses.
1463/// Users are responsible for serialization/deserialization of this data.
1464pub type HttpCacheMetadata = Vec<u8>;
1465
1466/// A closure that takes [`http::request::Parts`] and [`http::response::Parts`] and returns optional metadata to store with the cached response.
1467/// This allows middleware to compute and store additional information alongside cached responses.
1468pub type MetadataProvider = Arc<
1469 dyn Fn(&request::Parts, &response::Parts) -> Option<HttpCacheMetadata>
1470 + Send
1471 + Sync,
1472>;
1473
1474/// A closure that takes a mutable reference to [`HttpResponse`] and modifies it before caching.
1475pub type ModifyResponse = Arc<dyn Fn(&mut HttpResponse) + Send + Sync>;
1476
1477/// Configuration options for customizing HTTP cache behavior on a per-request basis.
1478///
1479/// This struct allows you to override default caching behavior for individual requests
1480/// by providing custom cache options, cache keys, cache modes, and cache busting logic.
1481///
1482/// # Examples
1483///
1484/// ## Basic Custom Cache Key
1485/// ```rust
1486/// use http_cache::{HttpCacheOptions, CacheKey};
1487/// use http::request::Parts;
1488/// use std::sync::Arc;
1489///
1490/// let options = HttpCacheOptions {
1491/// cache_key: Some(Arc::new(|parts: &Parts| {
1492/// format!("custom:{}:{}", parts.method, parts.uri.path())
1493/// })),
1494/// ..Default::default()
1495/// };
1496/// ```
1497///
1498/// ## Custom Cache Mode per Request
1499/// ```rust
1500/// use http_cache::{HttpCacheOptions, CacheMode, CacheModeFn};
1501/// use http::request::Parts;
1502/// use std::sync::Arc;
1503///
1504/// let options = HttpCacheOptions {
1505/// cache_mode_fn: Some(Arc::new(|parts: &Parts| {
1506/// if parts.headers.contains_key("x-no-cache") {
1507/// CacheMode::NoStore
1508/// } else {
1509/// CacheMode::Default
1510/// }
1511/// })),
1512/// ..Default::default()
1513/// };
1514/// ```
1515///
1516/// ## Response-Based Cache Mode Override
1517/// ```rust
1518/// use http_cache::{HttpCacheOptions, ResponseCacheModeFn, CacheMode};
1519/// use http::request::Parts;
1520/// use http_cache::HttpResponse;
1521/// use std::sync::Arc;
1522///
1523/// let options = HttpCacheOptions {
1524/// response_cache_mode_fn: Some(Arc::new(|_parts: &Parts, response: &HttpResponse| {
1525/// // Force cache 2xx responses even if headers say not to cache
1526/// if response.status >= 200 && response.status < 300 {
1527/// Some(CacheMode::ForceCache)
1528/// } else if response.status == 429 { // Rate limited
1529/// Some(CacheMode::NoStore) // Don't cache rate limit responses
1530/// } else {
1531/// None // Use default behavior
1532/// }
1533/// })),
1534/// ..Default::default()
1535/// };
1536/// ```
1537///
1538/// ## Content-Type Based Cache Mode Override
1539/// ```rust
1540/// use http_cache::{HttpCacheOptions, ResponseCacheModeFn, CacheMode};
1541/// use http::request::Parts;
1542/// use http_cache::HttpResponse;
1543/// use std::sync::Arc;
1544///
1545/// let options = HttpCacheOptions {
1546/// response_cache_mode_fn: Some(Arc::new(|_parts: &Parts, response: &HttpResponse| {
1547/// // Cache different content types with different strategies
1548/// if let Some(content_type) = response.headers.get("content-type") {
1549/// match content_type.as_str() {
1550/// ct if ct.starts_with("application/json") => Some(CacheMode::ForceCache),
1551/// ct if ct.starts_with("image/") => Some(CacheMode::Default),
1552/// ct if ct.starts_with("text/html") => Some(CacheMode::NoStore),
1553/// _ => None, // Use default behavior for other types
1554/// }
1555/// } else {
1556/// Some(CacheMode::NoStore) // No content-type = don't cache
1557/// }
1558/// })),
1559/// ..Default::default()
1560/// };
1561/// ```
1562///
1563/// ## Cache Busting for Related Resources
1564/// ```rust
1565/// use http_cache::{HttpCacheOptions, CacheBust, CacheKey};
1566/// use http::request::Parts;
1567/// use std::sync::Arc;
1568///
1569/// let options = HttpCacheOptions {
1570/// cache_bust: Some(Arc::new(|parts: &Parts, _cache_key: &Option<CacheKey>, _uri: &str| {
1571/// if parts.method == "POST" && parts.uri.path().starts_with("/api/users") {
1572/// vec![
1573/// "GET:/api/users".to_string(),
1574/// "GET:/api/users/list".to_string(),
1575/// ]
1576/// } else {
1577/// vec![]
1578/// }
1579/// })),
1580/// ..Default::default()
1581/// };
1582/// ```
1583///
1584/// ## Storing Metadata with Cached Responses
1585/// ```rust
1586/// use http_cache::{HttpCacheOptions, MetadataProvider};
1587/// use http::{request, response};
1588/// use std::sync::Arc;
1589///
1590/// let options = HttpCacheOptions {
1591/// metadata_provider: Some(Arc::new(|request_parts: &request::Parts, response_parts: &response::Parts| {
1592/// // Store computed information with the cached response
1593/// let content_type = response_parts
1594/// .headers
1595/// .get("content-type")
1596/// .and_then(|v| v.to_str().ok())
1597/// .unwrap_or("unknown");
1598///
1599/// // Return serialized metadata (users handle serialization)
1600/// Some(format!("path={};content-type={}", request_parts.uri.path(), content_type).into_bytes())
1601/// })),
1602/// ..Default::default()
1603/// };
1604/// ```
1605#[derive(Clone)]
1606pub struct HttpCacheOptions {
1607 /// Override the default cache options.
1608 pub cache_options: Option<CacheOptions>,
1609 /// Override the default cache key generator.
1610 ///
1611 /// **Note:** Custom closures receive only `&request::Parts` and do not receive
1612 /// the `override_method` parameter used for cache invalidation. This means
1613 /// RFC 7234 section 4.4 cross-method invalidation (e.g., a POST invalidating
1614 /// cached GET/HEAD entries) will not work correctly unless your closure
1615 /// differentiates keys by HTTP method.
1616 pub cache_key: Option<CacheKey>,
1617 /// Override the default cache mode.
1618 pub cache_mode_fn: Option<CacheModeFn>,
1619 /// Override cache behavior based on the response received.
1620 /// This function is called after receiving a response and can override
1621 /// the cache mode for that specific response. Returning `None` means
1622 /// use the default cache mode. This allows fine-grained control over
1623 /// caching behavior based on response status, headers, or content.
1624 pub response_cache_mode_fn: Option<ResponseCacheModeFn>,
1625 /// Bust the caches of the returned keys.
1626 pub cache_bust: Option<CacheBust>,
1627 /// Modifies the response before storing it in the cache.
1628 pub modify_response: Option<ModifyResponse>,
1629 /// Determines if the cache status headers should be added to the response.
1630 pub cache_status_headers: bool,
1631 /// Maximum time-to-live for cached responses.
1632 /// When set, this overrides any longer cache durations specified by the server.
1633 /// Particularly useful with `CacheMode::IgnoreRules` to provide expiration control.
1634 pub max_ttl: Option<Duration>,
1635 /// Rate limiter that applies only on cache misses.
1636 /// When enabled, requests that result in cache hits are returned immediately,
1637 /// while cache misses are rate limited before making network requests.
1638 /// This provides the optimal behavior for web scrapers and similar applications.
1639 #[cfg(feature = "rate-limiting")]
1640 pub rate_limiter: Option<Arc<dyn CacheAwareRateLimiter>>,
1641 /// Optional callback to provide metadata to store alongside cached responses.
1642 /// The callback receives request and response parts and can return metadata bytes.
1643 /// This is useful for storing computed information that should be associated with
1644 /// cached responses without recomputation on cache hits.
1645 pub metadata_provider: Option<MetadataProvider>,
1646}
1647
1648impl Default for HttpCacheOptions {
1649 fn default() -> Self {
1650 Self {
1651 cache_options: None,
1652 cache_key: None,
1653 cache_mode_fn: None,
1654 response_cache_mode_fn: None,
1655 cache_bust: None,
1656 modify_response: None,
1657 cache_status_headers: true,
1658 max_ttl: None,
1659 #[cfg(feature = "rate-limiting")]
1660 rate_limiter: None,
1661 metadata_provider: None,
1662 }
1663 }
1664}
1665
1666impl Debug for HttpCacheOptions {
1667 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1668 #[cfg(feature = "rate-limiting")]
1669 {
1670 f.debug_struct("HttpCacheOptions")
1671 .field("cache_options", &self.cache_options)
1672 .field("cache_key", &"Fn(&request::Parts) -> String")
1673 .field("cache_mode_fn", &"Fn(&request::Parts) -> CacheMode")
1674 .field(
1675 "response_cache_mode_fn",
1676 &"Fn(&request::Parts, &HttpResponse) -> Option<CacheMode>",
1677 )
1678 .field("cache_bust", &"Fn(&request::Parts) -> Vec<String>")
1679 .field("modify_response", &"Fn(&mut ModifyResponse)")
1680 .field("cache_status_headers", &self.cache_status_headers)
1681 .field("max_ttl", &self.max_ttl)
1682 .field("rate_limiter", &"Option<CacheAwareRateLimiter>")
1683 .field(
1684 "metadata_provider",
1685 &"Fn(&request::Parts, &response::Parts) -> Option<Vec<u8>>",
1686 )
1687 .finish()
1688 }
1689
1690 #[cfg(not(feature = "rate-limiting"))]
1691 {
1692 f.debug_struct("HttpCacheOptions")
1693 .field("cache_options", &self.cache_options)
1694 .field("cache_key", &"Fn(&request::Parts) -> String")
1695 .field("cache_mode_fn", &"Fn(&request::Parts) -> CacheMode")
1696 .field(
1697 "response_cache_mode_fn",
1698 &"Fn(&request::Parts, &HttpResponse) -> Option<CacheMode>",
1699 )
1700 .field("cache_bust", &"Fn(&request::Parts) -> Vec<String>")
1701 .field("modify_response", &"Fn(&mut ModifyResponse)")
1702 .field("cache_status_headers", &self.cache_status_headers)
1703 .field("max_ttl", &self.max_ttl)
1704 .field(
1705 "metadata_provider",
1706 &"Fn(&request::Parts, &response::Parts) -> Option<Vec<u8>>",
1707 )
1708 .finish()
1709 }
1710 }
1711}
1712
1713impl HttpCacheOptions {
1714 fn create_cache_key(
1715 &self,
1716 parts: &request::Parts,
1717 override_method: Option<&str>,
1718 ) -> String {
1719 if let Some(cache_key) = &self.cache_key {
1720 cache_key(parts)
1721 } else {
1722 format!(
1723 "{}:{}",
1724 override_method.unwrap_or_else(|| parts.method.as_str()),
1725 parts.uri
1726 )
1727 }
1728 }
1729
1730 /// Helper function for other crates to generate cache keys for invalidation
1731 /// This ensures consistent cache key generation across all implementations
1732 pub fn create_cache_key_for_invalidation(
1733 &self,
1734 parts: &request::Parts,
1735 method_override: &str,
1736 ) -> String {
1737 self.create_cache_key(parts, Some(method_override))
1738 }
1739
1740 /// Converts HttpResponse to http::Response with the given body type
1741 pub fn http_response_to_response<B>(
1742 http_response: &HttpResponse,
1743 body: B,
1744 ) -> Result<Response<B>> {
1745 let mut response_builder = Response::builder()
1746 .status(http_response.status)
1747 .version(http_response.version.into());
1748
1749 for (name, value) in &http_response.headers {
1750 if let (Ok(header_name), Ok(header_value)) =
1751 (name.parse::<http::HeaderName>(), value.parse::<HeaderValue>())
1752 {
1753 response_builder =
1754 response_builder.header(header_name, header_value);
1755 }
1756 }
1757
1758 Ok(response_builder.body(body)?)
1759 }
1760
1761 /// Converts response parts to HttpResponse format for cache mode evaluation
1762 fn parts_to_http_response(
1763 &self,
1764 parts: &response::Parts,
1765 request_parts: &request::Parts,
1766 metadata: Option<Vec<u8>>,
1767 ) -> Result<HttpResponse> {
1768 Ok(HttpResponse {
1769 body: vec![], // We don't need the full body for cache mode decision
1770 headers: (&parts.headers).into(),
1771 status: parts.status.as_u16(),
1772 url: extract_url_from_request_parts(request_parts)?,
1773 version: parts.version.try_into()?,
1774 metadata,
1775 })
1776 }
1777
1778 /// Evaluates response-based cache mode override
1779 fn evaluate_response_cache_mode(
1780 &self,
1781 request_parts: &request::Parts,
1782 http_response: &HttpResponse,
1783 original_mode: CacheMode,
1784 ) -> CacheMode {
1785 if let Some(response_cache_mode_fn) = &self.response_cache_mode_fn {
1786 if let Some(override_mode) =
1787 response_cache_mode_fn(request_parts, http_response)
1788 {
1789 return override_mode;
1790 }
1791 }
1792 original_mode
1793 }
1794
1795 /// Generates metadata for a response using the metadata_provider callback if configured
1796 pub fn generate_metadata(
1797 &self,
1798 request_parts: &request::Parts,
1799 response_parts: &response::Parts,
1800 ) -> Option<HttpCacheMetadata> {
1801 self.metadata_provider
1802 .as_ref()
1803 .and_then(|provider| provider(request_parts, response_parts))
1804 }
1805
1806 /// Modifies the response before caching if a modifier function is provided
1807 pub fn modify_response_before_caching(&self, response: &mut HttpResponse) {
1808 if let Some(modify_response) = &self.modify_response {
1809 modify_response(response);
1810 }
1811 }
1812
1813 /// Creates a cache policy for the given request and response
1814 fn create_cache_policy(
1815 &self,
1816 request_parts: &request::Parts,
1817 response_parts: &response::Parts,
1818 ) -> CachePolicy {
1819 let cache_options = self.cache_options.unwrap_or_default();
1820
1821 // If max_ttl is specified, we need to modify the response headers to enforce it
1822 if let Some(max_ttl) = self.max_ttl {
1823 // Parse existing cache-control header
1824 let cache_control = response_parts
1825 .headers
1826 .get("cache-control")
1827 .and_then(|v| v.to_str().ok())
1828 .unwrap_or("");
1829
1830 // Extract existing max-age if present
1831 let existing_max_age =
1832 cache_control.split(',').find_map(|directive| {
1833 let directive = directive.trim();
1834 if directive.starts_with("max-age=") {
1835 directive.strip_prefix("max-age=")?.parse::<u64>().ok()
1836 } else {
1837 None
1838 }
1839 });
1840
1841 // Convert max_ttl to seconds
1842 let max_ttl_seconds = max_ttl.as_secs();
1843
1844 // Apply max_ttl by setting max-age to the minimum of existing max-age and max_ttl
1845 let effective_max_age = match existing_max_age {
1846 Some(existing) => std::cmp::min(existing, max_ttl_seconds),
1847 None => max_ttl_seconds,
1848 };
1849
1850 // Build new cache-control header
1851 let mut new_directives = Vec::new();
1852
1853 // Add non-max-age directives from existing cache-control
1854 for directive in cache_control.split(',').map(|d| d.trim()) {
1855 if !directive.starts_with("max-age=") && !directive.is_empty() {
1856 new_directives.push(directive.to_string());
1857 }
1858 }
1859
1860 // Add our effective max-age
1861 new_directives.push(format!("max-age={}", effective_max_age));
1862
1863 let new_cache_control = new_directives.join(", ");
1864
1865 // Create modified response parts - we have to clone since response::Parts has private fields
1866 let mut modified_response_parts = response_parts.clone();
1867 modified_response_parts.headers.insert(
1868 "cache-control",
1869 HeaderValue::from_str(&new_cache_control)
1870 .unwrap_or_else(|_| HeaderValue::from_static("max-age=0")),
1871 );
1872
1873 CachePolicy::new_options(
1874 request_parts,
1875 &modified_response_parts,
1876 SystemTime::now(),
1877 cache_options,
1878 )
1879 } else {
1880 CachePolicy::new_options(
1881 request_parts,
1882 response_parts,
1883 SystemTime::now(),
1884 cache_options,
1885 )
1886 }
1887 }
1888
1889 /// Determines if a response should be cached based on cache mode and HTTP semantics
1890 fn should_cache_response(
1891 &self,
1892 effective_cache_mode: CacheMode,
1893 http_response: &HttpResponse,
1894 is_get_head: bool,
1895 policy: &CachePolicy,
1896 ) -> bool {
1897 // HTTP status codes that are cacheable by default (RFC 7234)
1898 let is_cacheable_status = matches!(
1899 http_response.status,
1900 200 | 203 | 204 | 206 | 300 | 301 | 404 | 405 | 410 | 414 | 501
1901 );
1902
1903 if is_cacheable_status {
1904 match effective_cache_mode {
1905 CacheMode::ForceCache => is_get_head,
1906 CacheMode::IgnoreRules => true,
1907 CacheMode::NoStore => false,
1908 _ => is_get_head && policy.is_storable(),
1909 }
1910 } else {
1911 false
1912 }
1913 }
1914
1915 /// Common request analysis logic shared between streaming and non-streaming implementations
1916 fn analyze_request_internal(
1917 &self,
1918 parts: &request::Parts,
1919 mode_override: Option<CacheMode>,
1920 default_mode: CacheMode,
1921 ) -> Result<CacheAnalysis> {
1922 let effective_mode = mode_override
1923 .or_else(|| self.cache_mode_fn.as_ref().map(|f| f(parts)))
1924 .unwrap_or(default_mode);
1925
1926 let is_get_head = parts.method == "GET" || parts.method == "HEAD";
1927 let should_cache = effective_mode == CacheMode::IgnoreRules
1928 || (is_get_head && effective_mode != CacheMode::NoStore);
1929
1930 let cache_key = self.create_cache_key(parts, None);
1931
1932 let cache_bust_keys = if let Some(cache_bust) = &self.cache_bust {
1933 cache_bust(parts, &self.cache_key, &cache_key)
1934 } else {
1935 Vec::new()
1936 };
1937
1938 Ok(CacheAnalysis {
1939 cache_key,
1940 should_cache,
1941 cache_mode: effective_mode,
1942 cache_bust_keys,
1943 request_parts: parts.clone(),
1944 is_get_head,
1945 })
1946 }
1947}
1948
1949/// Caches requests according to http spec.
1950#[derive(Debug, Clone)]
1951pub struct HttpCache<T: CacheManager> {
1952 /// Determines the manager behavior.
1953 pub mode: CacheMode,
1954 /// Manager instance that implements the [`CacheManager`] trait.
1955 /// By default, a manager implementation with [`cacache`](https://github.com/zkat/cacache-rs)
1956 /// as the backend has been provided, see [`CACacheManager`].
1957 pub manager: T,
1958 /// Override the default cache options.
1959 pub options: HttpCacheOptions,
1960}
1961
1962/// Wrapper for user metadata stored in response extensions during cache reads.
1963/// Used to preserve metadata through 304 re-cache operations so that
1964/// `StreamingCacheManager::put` receives the original metadata instead of
1965/// regenerating it (which may produce different or empty results).
1966#[derive(Debug, Clone)]
1967pub(crate) struct CachedUserMetadata(pub Option<Vec<u8>>);
1968
1969/// Streaming version of HTTP cache that supports streaming request/response bodies
1970/// without buffering them in memory.
1971#[derive(Debug, Clone)]
1972pub struct HttpStreamingCache<T: StreamingCacheManager> {
1973 /// Determines the manager behavior.
1974 pub mode: CacheMode,
1975 /// Manager instance that implements the [`StreamingCacheManager`] trait.
1976 pub manager: T,
1977 /// Override the default cache options.
1978 pub options: HttpCacheOptions,
1979}
1980
1981// ============================================================================
1982// Helper functions for working with warning headers on http::Response
1983// ============================================================================
1984
1985/// Extracts the warning code from an `http::Response`'s warning header, if
1986/// present. Returns the 3-digit warn-code as a `usize`.
1987fn response_warning_code<B>(response: &Response<B>) -> Option<usize> {
1988 response
1989 .headers()
1990 .get(WARNING)
1991 .and_then(|hdr| hdr.to_str().ok())
1992 .and_then(|s| s.chars().take(3).collect::<String>().parse().ok())
1993}
1994
1995/// Adds an RFC 2616 §14.46 warning header to an `http::Response`.
1996fn response_add_warning<B>(
1997 response: &mut Response<B>,
1998 url: &Url,
1999 code: usize,
2000 message: &str,
2001) {
2002 let host = url_host_str(url);
2003 let escaped_message = message.replace('"', "'").replace(['\n', '\r'], " ");
2004 let value = format!(
2005 "{} {} \"{}\" \"{}\"",
2006 code,
2007 host,
2008 escaped_message,
2009 httpdate::fmt_http_date(SystemTime::now()),
2010 );
2011 if let Ok(hv) = HeaderValue::from_str(&value) {
2012 response.headers_mut().insert(WARNING, hv);
2013 }
2014}
2015
2016/// Removes the warning header from an `http::Response`.
2017fn response_remove_warning<B>(response: &mut Response<B>) {
2018 response.headers_mut().remove(WARNING);
2019}
2020
2021/// Returns `true` if the `cache-control` header of the response contains the
2022/// `must-revalidate` directive.
2023fn response_must_revalidate<B>(response: &Response<B>) -> bool {
2024 response
2025 .headers()
2026 .get(CACHE_CONTROL)
2027 .and_then(|v| v.to_str().ok())
2028 .is_some_and(|val| val.to_lowercase().contains("must-revalidate"))
2029}
2030
2031/// Adds the custom `x-cache` status header to an `http::Response`.
2032fn response_cache_status<B>(
2033 response: &mut Response<B>,
2034 hit_or_miss: HitOrMiss,
2035) {
2036 if let Ok(hv) = HeaderValue::from_str(&hit_or_miss.to_string()) {
2037 response.headers_mut().insert(XCACHE, hv);
2038 }
2039}
2040
2041/// Adds the custom `x-cache-lookup` status header to an `http::Response`.
2042fn response_cache_lookup_status<B>(
2043 response: &mut Response<B>,
2044 hit_or_miss: HitOrMiss,
2045) {
2046 if let Ok(hv) = HeaderValue::from_str(&hit_or_miss.to_string()) {
2047 response.headers_mut().insert(XCACHELOOKUP, hv);
2048 }
2049}
2050
2051/// Applies [`HttpCacheOptions::modify_response_before_caching`] to a
2052/// streaming `Response<B>`. The callback expects `&mut HttpResponse`, so we
2053/// build a temporary shim with the response's headers/status (empty body),
2054/// call the callback, and copy any header or status changes back.
2055///
2056/// Body and metadata modifications made by the callback are not reflected
2057/// because the streaming body is not buffered.
2058fn apply_modify_response_shim<B>(
2059 options: &HttpCacheOptions,
2060 response: &mut Response<B>,
2061 url: &Url,
2062) {
2063 let modify = match &options.modify_response {
2064 Some(f) => f,
2065 None => return,
2066 };
2067 let mut shim = HttpResponse {
2068 body: Vec::new(),
2069 headers: HttpHeaders::from(response.headers()),
2070 status: response.status().as_u16(),
2071 url: url.clone(),
2072 version: response.version().try_into().unwrap_or(HttpVersion::Http11),
2073 metadata: None,
2074 };
2075 modify(&mut shim);
2076 // Apply header changes back
2077 response.headers_mut().clear();
2078 for (name, value) in shim.headers.iter() {
2079 if let (Ok(hn), Ok(hv)) = (
2080 http::header::HeaderName::from_bytes(name.as_bytes()),
2081 HeaderValue::from_str(value),
2082 ) {
2083 response.headers_mut().append(hn, hv);
2084 }
2085 }
2086 // Apply status change back
2087 if let Ok(new_status) = StatusCode::from_u16(shim.status) {
2088 *response.status_mut() = new_status;
2089 }
2090}
2091
2092// ============================================================================
2093// HttpStreamingCache orchestrator methods
2094// ============================================================================
2095
2096impl<T: StreamingCacheManager> HttpStreamingCache<T>
2097where
2098 <T::Body as http_body::Body>::Data: Send,
2099 <T::Body as http_body::Body>::Error:
2100 Into<StreamingError> + Send + Sync + 'static,
2101{
2102 /// Determines if the request described by `parts` should be cached,
2103 /// taking into account any `mode_override`.
2104 pub fn can_cache_request(
2105 &self,
2106 parts: &request::Parts,
2107 mode_override: Option<CacheMode>,
2108 ) -> Result<bool> {
2109 let analysis = <Self as HttpCacheStreamInterface>::analyze_request(
2110 self,
2111 parts,
2112 mode_override,
2113 )?;
2114 Ok(analysis.should_cache)
2115 }
2116
2117 /// Apply rate limiting if enabled in options.
2118 #[cfg(feature = "rate-limiting")]
2119 async fn apply_rate_limiting(&self, url: &Url) {
2120 if let Some(rate_limiter) = &self.options.rate_limiter {
2121 let rate_limit_key = url_hostname(url).unwrap_or("unknown");
2122 rate_limiter.until_key_ready(rate_limit_key).await;
2123 }
2124 }
2125
2126 /// Apply rate limiting if enabled in options (no-op without
2127 /// rate-limiting feature).
2128 #[cfg(not(feature = "rate-limiting"))]
2129 async fn apply_rate_limiting(&self, _url: &Url) {
2130 // No-op when rate limiting feature is not enabled
2131 }
2132
2133 /// Performs cache-busting housekeeping for requests that should not be
2134 /// cached. Mirrors [`HttpCache::run_no_cache`].
2135 pub async fn run_no_cache(&self, parts: &request::Parts) -> Result<()> {
2136 self.manager
2137 .delete(&self.options.create_cache_key(parts, Some("GET")))
2138 .await
2139 .ok();
2140 self.manager
2141 .delete(&self.options.create_cache_key(parts, Some("HEAD")))
2142 .await
2143 .ok();
2144
2145 let cache_key = self.options.create_cache_key(parts, None);
2146
2147 if let Some(cache_bust) = &self.options.cache_bust {
2148 for key_to_cache_bust in
2149 cache_bust(parts, &self.options.cache_key, &cache_key)
2150 {
2151 self.manager.delete(&key_to_cache_bust).await?;
2152 }
2153 }
2154
2155 Ok(())
2156 }
2157
2158 /// The main streaming cache orchestrator.
2159 ///
2160 /// This mirrors the logic of [`HttpCache::run`] but operates on
2161 /// streaming `Response<B>` bodies and delegates upstream fetching to a
2162 /// caller-supplied callback instead of an `impl Middleware`.
2163 ///
2164 /// # Arguments
2165 ///
2166 /// * `parts` - The request parts to evaluate.
2167 /// * `mode_override` - Optional per-request cache mode override.
2168 /// * `fetch` - A callback that performs the actual HTTP request. It
2169 /// receives a [`FetchRequest`] indicating whether to issue a fresh or
2170 /// conditional request, and must return the upstream `Response<B>`.
2171 /// Called at most once per request.
2172 pub async fn run<B, F, Fut>(
2173 &self,
2174 parts: &request::Parts,
2175 mode_override: Option<CacheMode>,
2176 fetch: F,
2177 ) -> Result<Response<T::Body>>
2178 where
2179 B: http_body::Body + Send + 'static,
2180 B::Data: Send,
2181 B::Error: Into<StreamingError>,
2182 F: FnOnce(FetchRequest) -> Fut,
2183 Fut: Future<Output = Result<Response<B>>>,
2184 {
2185 // 1. Analyze the request
2186 let analysis = <Self as HttpCacheStreamInterface>::analyze_request(
2187 self,
2188 parts,
2189 mode_override,
2190 )?;
2191
2192 // 2. If the request should not be cached, fetch and process as a
2193 // remote miss.
2194 if !analysis.should_cache {
2195 let url = extract_url_from_request_parts(parts)?;
2196 self.apply_rate_limiting(&url).await;
2197 let response = fetch(FetchRequest::Fresh).await?;
2198 return self.remote_fetch_and_cache(analysis, response).await;
2199 }
2200
2201 // 3. Bust cache keys if needed
2202 for key in &analysis.cache_bust_keys {
2203 self.manager.delete(key).await?;
2204 }
2205
2206 // 4. Look up cached response
2207 if let Some((mut cached_response, policy)) =
2208 <Self as HttpCacheStreamInterface>::lookup_cached_response(
2209 self,
2210 &analysis.cache_key,
2211 )
2212 .await?
2213 {
2214 if self.options.cache_status_headers {
2215 response_cache_lookup_status(
2216 &mut cached_response,
2217 HitOrMiss::HIT,
2218 );
2219 }
2220
2221 // Handle warning headers per RFC 7234 §4.3.4
2222 if let Some(warning_code) = response_warning_code(&cached_response)
2223 {
2224 if (100..200).contains(&warning_code) {
2225 response_remove_warning(&mut cached_response);
2226 }
2227 }
2228
2229 // 5. Branch on cache mode
2230 match analysis.cache_mode {
2231 CacheMode::Default => {
2232 self.conditional_fetch(
2233 &analysis,
2234 fetch,
2235 cached_response,
2236 policy,
2237 )
2238 .await
2239 }
2240 CacheMode::NoCache => {
2241 // Force a fresh fetch with no-cache directive, but
2242 // note that we had a cache lookup hit.
2243 let url = extract_url_from_request_parts(parts)?;
2244 self.apply_rate_limiting(&url).await;
2245 let response = fetch(FetchRequest::FreshNoCache).await?;
2246 let mut res =
2247 self.remote_fetch_and_cache(analysis, response).await?;
2248 if self.options.cache_status_headers {
2249 response_cache_lookup_status(&mut res, HitOrMiss::HIT);
2250 }
2251 Ok(res)
2252 }
2253 CacheMode::ForceCache
2254 | CacheMode::OnlyIfCached
2255 | CacheMode::IgnoreRules => {
2256 // 112 Disconnected operation
2257 // SHOULD be included if the cache is intentionally
2258 // disconnected from the rest of the network for a
2259 // period of time.
2260 // (https://tools.ietf.org/html/rfc2616#section-14.46)
2261 let url = extract_url_from_request_parts(parts)?;
2262 response_add_warning(
2263 &mut cached_response,
2264 &url,
2265 112,
2266 "Disconnected operation",
2267 );
2268 if self.options.cache_status_headers {
2269 response_cache_status(
2270 &mut cached_response,
2271 HitOrMiss::HIT,
2272 );
2273 }
2274 Ok(cached_response)
2275 }
2276 CacheMode::Reload => {
2277 let url = extract_url_from_request_parts(parts)?;
2278 self.apply_rate_limiting(&url).await;
2279 let response = fetch(FetchRequest::Fresh).await?;
2280 let mut res =
2281 self.remote_fetch_and_cache(analysis, response).await?;
2282 if self.options.cache_status_headers {
2283 response_cache_lookup_status(&mut res, HitOrMiss::HIT);
2284 }
2285 Ok(res)
2286 }
2287 _ => {
2288 let url = extract_url_from_request_parts(parts)?;
2289 self.apply_rate_limiting(&url).await;
2290 let response = fetch(FetchRequest::Fresh).await?;
2291 self.remote_fetch_and_cache(analysis, response).await
2292 }
2293 }
2294 } else {
2295 // 6. No cached response found
2296 match analysis.cache_mode {
2297 CacheMode::OnlyIfCached => {
2298 // ENOTCACHED — return 504 Gateway Timeout
2299 let mut res = Response::builder()
2300 .status(StatusCode::GATEWAY_TIMEOUT)
2301 .body(self.manager.empty_body())
2302 .map_err(|e| -> BoxError { e.into() })?;
2303 if self.options.cache_status_headers {
2304 response_cache_status(&mut res, HitOrMiss::MISS);
2305 response_cache_lookup_status(&mut res, HitOrMiss::MISS);
2306 }
2307 Ok(res)
2308 }
2309 _ => {
2310 let url = extract_url_from_request_parts(parts)?;
2311 self.apply_rate_limiting(&url).await;
2312 let response = fetch(FetchRequest::Fresh).await?;
2313 self.remote_fetch_and_cache(analysis, response).await
2314 }
2315 }
2316 }
2317 }
2318
2319 /// Processes a fresh upstream response and potentially caches it.
2320 ///
2321 /// Mirrors [`HttpCache::remote_fetch`] but receives the response
2322 /// directly rather than calling middleware. Rate limiting is performed
2323 /// by the caller before invoking `fetch`.
2324 async fn remote_fetch_and_cache<B>(
2325 &self,
2326 analysis: CacheAnalysis,
2327 response: Response<B>,
2328 ) -> Result<Response<T::Body>>
2329 where
2330 B: http_body::Body + Send + 'static,
2331 B::Data: Send,
2332 B::Error: Into<StreamingError>,
2333 {
2334 // Delegate to process_response which handles:
2335 // - response-based cache mode override evaluation
2336 // - policy creation
2337 // - should_cache_response check
2338 // - cache busting for non-GET/HEAD
2339 // - storing via manager.put or converting via manager.convert_body
2340 // - adding cache status headers (MISS/MISS)
2341 // - applying modify_response_before_caching shim before put
2342 let res = <Self as HttpCacheStreamInterface>::process_response(
2343 self,
2344 analysis.clone(),
2345 response,
2346 None,
2347 )
2348 .await?;
2349
2350 Ok(res)
2351 }
2352
2353 /// Performs a conditional fetch (revalidation) against the origin,
2354 /// returning either the still-valid cached response or the fresh
2355 /// upstream response.
2356 ///
2357 /// Mirrors [`HttpCache::conditional_fetch`].
2358 async fn conditional_fetch<B, F, Fut>(
2359 &self,
2360 analysis: &CacheAnalysis,
2361 fetch: F,
2362 mut cached_res: Response<T::Body>,
2363 mut policy: CachePolicy,
2364 ) -> Result<Response<T::Body>>
2365 where
2366 B: http_body::Body + Send + 'static,
2367 B::Data: Send,
2368 B::Error: Into<StreamingError>,
2369 F: FnOnce(FetchRequest) -> Fut,
2370 Fut: Future<Output = Result<Response<B>>>,
2371 {
2372 let parts = &analysis.request_parts;
2373 let before_req = policy.before_request(parts, SystemTime::now());
2374 match before_req {
2375 BeforeRequest::Fresh(fresh_parts) => {
2376 // Update headers from the policy result, preserving multi-valued headers
2377 for name in fresh_parts.headers.keys() {
2378 cached_res.headers_mut().remove(name);
2379 }
2380 for (name, value) in fresh_parts.headers.iter() {
2381 cached_res
2382 .headers_mut()
2383 .append(name.clone(), value.clone());
2384 }
2385 if self.options.cache_status_headers {
2386 response_cache_status(&mut cached_res, HitOrMiss::HIT);
2387 response_cache_lookup_status(
2388 &mut cached_res,
2389 HitOrMiss::HIT,
2390 );
2391 }
2392 Ok(cached_res)
2393 }
2394 BeforeRequest::Stale { request: stale_parts, matches } => {
2395 let req_url = extract_url_from_request_parts(parts)?;
2396 // Apply rate limiting before revalidation request
2397 self.apply_rate_limiting(&req_url).await;
2398
2399 // Only send conditional headers when matches is true
2400 // (matching reference behavior at HttpCache::conditional_fetch)
2401 let fetch_result = if matches {
2402 fetch(FetchRequest::Conditional(Box::new(stale_parts)))
2403 .await
2404 } else {
2405 fetch(FetchRequest::Fresh).await
2406 };
2407
2408 match fetch_result {
2409 Ok(cond_res) => {
2410 let status = cond_res.status();
2411
2412 if status.is_server_error()
2413 && response_must_revalidate(&cached_res)
2414 {
2415 // 111 Revalidation failed
2416 // MUST be included if a cache returns a
2417 // stale response because an attempt to
2418 // revalidate the response failed, due to an
2419 // inability to reach the server.
2420 // (https://tools.ietf.org/html/rfc2616#section-14.46)
2421 response_add_warning(
2422 &mut cached_res,
2423 &req_url,
2424 111,
2425 "Revalidation failed",
2426 );
2427 if self.options.cache_status_headers {
2428 response_cache_status(
2429 &mut cached_res,
2430 HitOrMiss::HIT,
2431 );
2432 }
2433 Ok(cached_res)
2434 } else if status == StatusCode::NOT_MODIFIED {
2435 // 304 Not Modified — update cached response
2436 // headers using policy.after_response
2437 let (cond_parts, _cond_body) =
2438 cond_res.into_parts();
2439 let after_res = policy.after_response(
2440 parts,
2441 &cond_parts,
2442 SystemTime::now(),
2443 );
2444 match after_res {
2445 AfterResponse::Modified(
2446 new_policy,
2447 new_parts,
2448 )
2449 | AfterResponse::NotModified(
2450 new_policy,
2451 new_parts,
2452 ) => {
2453 policy = new_policy;
2454 // Update cached response headers, preserving multi-valued headers
2455 for name in new_parts.headers.keys() {
2456 cached_res.headers_mut().remove(name);
2457 }
2458 for (name, value) in
2459 new_parts.headers.iter()
2460 {
2461 cached_res.headers_mut().append(
2462 name.clone(),
2463 value.clone(),
2464 );
2465 }
2466 }
2467 }
2468 if self.options.cache_status_headers {
2469 response_cache_status(
2470 &mut cached_res,
2471 HitOrMiss::HIT,
2472 );
2473 response_cache_lookup_status(
2474 &mut cached_res,
2475 HitOrMiss::HIT,
2476 );
2477 }
2478
2479 apply_modify_response_shim(
2480 &self.options,
2481 &mut cached_res,
2482 &req_url,
2483 );
2484
2485 // Re-cache the updated response. Preserve
2486 // original user metadata from the cached
2487 // response instead of regenerating it (which
2488 // may produce different or empty results).
2489 let request_url =
2490 extract_url_from_request_parts(parts)?;
2491 let metadata = cached_res
2492 .extensions()
2493 .get::<CachedUserMetadata>()
2494 .and_then(|m| m.0.clone());
2495
2496 let res = self
2497 .manager
2498 .put(
2499 self.options.create_cache_key(parts, None),
2500 cached_res,
2501 policy,
2502 request_url,
2503 metadata,
2504 )
2505 .await?;
2506 Ok(res)
2507 } else if status == StatusCode::OK {
2508 // 200 OK — fresh response, create new policy
2509 // and cache
2510 let (cond_parts, cond_body) = cond_res.into_parts();
2511 let new_policy = self
2512 .options
2513 .create_cache_policy(parts, &cond_parts);
2514 let metadata = self
2515 .options
2516 .generate_metadata(parts, &cond_parts);
2517 let cond_res =
2518 Response::from_parts(cond_parts, cond_body);
2519
2520 let request_url =
2521 extract_url_from_request_parts(parts)?;
2522
2523 // Apply modify_response BEFORE cacheability checks
2524 // (matches non-streaming reference order)
2525 let mut cond_res = cond_res;
2526 apply_modify_response_shim(
2527 &self.options,
2528 &mut cond_res,
2529 &request_url,
2530 );
2531
2532 // Build HttpResponse shim from modified response
2533 let http_response_shim = HttpResponse {
2534 body: vec![],
2535 headers: cond_res.headers().into(),
2536 status: cond_res.status().as_u16(),
2537 url: request_url.clone(),
2538 version: cond_res
2539 .version()
2540 .try_into()
2541 .unwrap_or(HttpVersion::Http11),
2542 metadata: metadata.clone(),
2543 };
2544 // Apply response-based cache mode override
2545 let effective_mode =
2546 self.options.evaluate_response_cache_mode(
2547 parts,
2548 &http_response_shim,
2549 analysis.cache_mode,
2550 );
2551 let is_cacheable =
2552 self.options.should_cache_response(
2553 effective_mode,
2554 &http_response_shim,
2555 analysis.is_get_head,
2556 &new_policy,
2557 );
2558
2559 // Set cache status headers
2560 if self.options.cache_status_headers {
2561 response_cache_status(
2562 &mut cond_res,
2563 HitOrMiss::MISS,
2564 );
2565 response_cache_lookup_status(
2566 &mut cond_res,
2567 HitOrMiss::HIT,
2568 );
2569 }
2570
2571 if is_cacheable {
2572 let res = self
2573 .manager
2574 .put(
2575 self.options
2576 .create_cache_key(parts, None),
2577 cond_res,
2578 new_policy,
2579 request_url,
2580 metadata,
2581 )
2582 .await?;
2583 Ok(res)
2584 } else {
2585 let res =
2586 self.manager.convert_body(cond_res).await?;
2587 Ok(res)
2588 }
2589 } else {
2590 // Any other status — return fresh response
2591 let mut res =
2592 self.manager.convert_body(cond_res).await?;
2593 if self.options.cache_status_headers {
2594 response_cache_status(
2595 &mut res,
2596 HitOrMiss::MISS,
2597 );
2598 response_cache_lookup_status(
2599 &mut res,
2600 HitOrMiss::HIT,
2601 );
2602 }
2603 Ok(res)
2604 }
2605 }
2606 Err(e) => {
2607 if response_must_revalidate(&cached_res) {
2608 Err(e)
2609 } else {
2610 // 111 Revalidation failed
2611 // MUST be included if a cache returns a
2612 // stale response because an attempt to
2613 // revalidate the response failed, due to an
2614 // inability to reach the server.
2615 // (https://tools.ietf.org/html/rfc2616#section-14.46)
2616 response_add_warning(
2617 &mut cached_res,
2618 &req_url,
2619 111,
2620 "Revalidation failed",
2621 );
2622 if self.options.cache_status_headers {
2623 response_cache_status(
2624 &mut cached_res,
2625 HitOrMiss::HIT,
2626 );
2627 }
2628 Ok(cached_res)
2629 }
2630 }
2631 }
2632 }
2633 }
2634 }
2635}
2636
2637impl<T: CacheManager> HttpCache<T> {
2638 /// Determines if the request should be cached
2639 pub fn can_cache_request(
2640 &self,
2641 middleware: &impl Middleware,
2642 ) -> Result<bool> {
2643 let analysis = self.analyze_request(
2644 &middleware.parts()?,
2645 middleware.overridden_cache_mode(),
2646 )?;
2647 Ok(analysis.should_cache)
2648 }
2649
2650 /// Apply rate limiting if enabled in options
2651 #[cfg(feature = "rate-limiting")]
2652 async fn apply_rate_limiting(&self, url: &Url) {
2653 if let Some(rate_limiter) = &self.options.rate_limiter {
2654 let rate_limit_key = url_hostname(url).unwrap_or("unknown");
2655 rate_limiter.until_key_ready(rate_limit_key).await;
2656 }
2657 }
2658
2659 /// Apply rate limiting if enabled in options (no-op without rate-limiting feature)
2660 #[cfg(not(feature = "rate-limiting"))]
2661 async fn apply_rate_limiting(&self, _url: &Url) {
2662 // No-op when rate limiting feature is not enabled
2663 }
2664
2665 /// Cache-busting for non-cacheable requests, taking pre-extracted parts.
2666 pub async fn run_no_cache_from_parts(
2667 &self,
2668 parts: &request::Parts,
2669 ) -> Result<()> {
2670 self.manager
2671 .delete(&self.options.create_cache_key(parts, Some("GET")))
2672 .await
2673 .ok();
2674 self.manager
2675 .delete(&self.options.create_cache_key(parts, Some("HEAD")))
2676 .await
2677 .ok();
2678
2679 let cache_key = self.options.create_cache_key(parts, None);
2680
2681 if let Some(cache_bust) = &self.options.cache_bust {
2682 for key_to_cache_bust in
2683 cache_bust(parts, &self.options.cache_key, &cache_key)
2684 {
2685 self.manager.delete(&key_to_cache_bust).await?;
2686 }
2687 }
2688
2689 Ok(())
2690 }
2691
2692 /// Runs the actions to perform when the client middleware is running without the cache
2693 pub async fn run_no_cache(
2694 &self,
2695 middleware: &mut impl Middleware,
2696 ) -> Result<()> {
2697 let parts = middleware.parts()?;
2698 self.run_no_cache_from_parts(&parts).await
2699 }
2700
2701 /// Attempts to run the passed middleware along with the cache
2702 pub async fn run(
2703 &self,
2704 mut middleware: impl Middleware,
2705 ) -> Result<HttpResponse> {
2706 // Use the HttpCacheInterface to analyze the request
2707 let analysis = self.analyze_request(
2708 &middleware.parts()?,
2709 middleware.overridden_cache_mode(),
2710 )?;
2711
2712 if !analysis.should_cache {
2713 return self.remote_fetch(&mut middleware).await;
2714 }
2715
2716 // Bust cache keys if needed
2717 for key in &analysis.cache_bust_keys {
2718 self.manager.delete(key).await?;
2719 }
2720
2721 // Look up cached response
2722 if let Some((mut cached_response, policy)) =
2723 self.lookup_cached_response(&analysis.cache_key).await?
2724 {
2725 if self.options.cache_status_headers {
2726 cached_response.cache_lookup_status(HitOrMiss::HIT);
2727 }
2728
2729 // Handle warning headers
2730 if let Some(warning_code) = cached_response.warning_code() {
2731 // https://tools.ietf.org/html/rfc7234#section-4.3.4
2732 //
2733 // If a stored response is selected for update, the cache MUST:
2734 //
2735 // * delete any warning header fields in the stored response with
2736 // warn-code 1xx (see Section 5.5);
2737 //
2738 // * retain any warning header fields in the stored response with
2739 // warn-code 2xx;
2740 //
2741 if (100..200).contains(&warning_code) {
2742 cached_response.remove_warning();
2743 }
2744 }
2745
2746 match analysis.cache_mode {
2747 CacheMode::Default => {
2748 self.conditional_fetch(middleware, cached_response, policy)
2749 .await
2750 }
2751 CacheMode::NoCache => {
2752 middleware.force_no_cache()?;
2753 let mut res = self.remote_fetch(&mut middleware).await?;
2754 if self.options.cache_status_headers {
2755 res.cache_lookup_status(HitOrMiss::HIT);
2756 }
2757 Ok(res)
2758 }
2759 CacheMode::ForceCache
2760 | CacheMode::OnlyIfCached
2761 | CacheMode::IgnoreRules => {
2762 // 112 Disconnected operation
2763 // SHOULD be included if the cache is intentionally disconnected from
2764 // the rest of the network for a period of time.
2765 // (https://tools.ietf.org/html/rfc2616#section-14.46)
2766 cached_response.add_warning(
2767 &cached_response.url.clone(),
2768 112,
2769 "Disconnected operation",
2770 );
2771 if self.options.cache_status_headers {
2772 cached_response.cache_status(HitOrMiss::HIT);
2773 }
2774 Ok(cached_response)
2775 }
2776 CacheMode::Reload => {
2777 let mut res = self.remote_fetch(&mut middleware).await?;
2778 if self.options.cache_status_headers {
2779 res.cache_lookup_status(HitOrMiss::HIT);
2780 }
2781 Ok(res)
2782 }
2783 _ => self.remote_fetch(&mut middleware).await,
2784 }
2785 } else {
2786 match analysis.cache_mode {
2787 CacheMode::OnlyIfCached => {
2788 // ENOTCACHED
2789 let mut res = HttpResponse {
2790 body: Vec::new(),
2791 headers: HttpHeaders::default(),
2792 status: 504,
2793 url: middleware.url()?,
2794 version: HttpVersion::Http11,
2795 metadata: None,
2796 };
2797 if self.options.cache_status_headers {
2798 res.cache_status(HitOrMiss::MISS);
2799 res.cache_lookup_status(HitOrMiss::MISS);
2800 }
2801 Ok(res)
2802 }
2803 _ => self.remote_fetch(&mut middleware).await,
2804 }
2805 }
2806 }
2807
2808 fn cache_mode(&self, middleware: &impl Middleware) -> Result<CacheMode> {
2809 Ok(if let Some(mode) = middleware.overridden_cache_mode() {
2810 mode
2811 } else if let Some(cache_mode_fn) = &self.options.cache_mode_fn {
2812 cache_mode_fn(&middleware.parts()?)
2813 } else {
2814 self.mode
2815 })
2816 }
2817
2818 async fn remote_fetch(
2819 &self,
2820 middleware: &mut impl Middleware,
2821 ) -> Result<HttpResponse> {
2822 // Apply rate limiting before making the network request
2823 let url = middleware.url()?;
2824 self.apply_rate_limiting(&url).await;
2825
2826 let mut res = middleware.remote_fetch().await?;
2827 if self.options.cache_status_headers {
2828 res.cache_status(HitOrMiss::MISS);
2829 res.cache_lookup_status(HitOrMiss::MISS);
2830 }
2831 let policy = match self.options.cache_options {
2832 Some(options) => middleware.policy_with_options(&res, options)?,
2833 None => middleware.policy(&res)?,
2834 };
2835 let is_get_head = middleware.is_method_get_head();
2836 let mut mode = self.cache_mode(middleware)?;
2837 let parts = middleware.parts()?;
2838
2839 // Allow response-based cache mode override
2840 if let Some(response_cache_mode_fn) =
2841 &self.options.response_cache_mode_fn
2842 {
2843 if let Some(override_mode) = response_cache_mode_fn(&parts, &res) {
2844 mode = override_mode;
2845 }
2846 }
2847
2848 let is_cacheable = self.options.should_cache_response(
2849 mode,
2850 &res,
2851 is_get_head,
2852 &policy,
2853 );
2854
2855 if is_cacheable {
2856 // Generate metadata using the provider callback if configured
2857 let response_parts = res.parts()?;
2858 res.metadata =
2859 self.options.generate_metadata(&parts, &response_parts);
2860
2861 self.options.modify_response_before_caching(&mut res);
2862 let res = self
2863 .manager
2864 .put(self.options.create_cache_key(&parts, None), res, policy)
2865 .await?;
2866 // RFC 7234 s4.4: invalidate GET and HEAD keys for non-GET/HEAD successful responses
2867 if !is_get_head {
2868 let status = StatusCode::from_u16(res.status)?;
2869 if status.is_success() || status.is_redirection() {
2870 self.manager
2871 .delete(
2872 &self.options.create_cache_key(&parts, Some("GET")),
2873 )
2874 .await
2875 .ok();
2876 self.manager
2877 .delete(
2878 &self
2879 .options
2880 .create_cache_key(&parts, Some("HEAD")),
2881 )
2882 .await
2883 .ok();
2884 }
2885 }
2886 Ok(res)
2887 } else if !is_get_head {
2888 let status = StatusCode::from_u16(res.status)?;
2889 if status.is_success() || status.is_redirection() {
2890 self.manager
2891 .delete(&self.options.create_cache_key(&parts, Some("GET")))
2892 .await
2893 .ok();
2894 self.manager
2895 .delete(
2896 &self.options.create_cache_key(&parts, Some("HEAD")),
2897 )
2898 .await
2899 .ok();
2900 }
2901 Ok(res)
2902 } else {
2903 Ok(res)
2904 }
2905 }
2906
2907 async fn conditional_fetch(
2908 &self,
2909 mut middleware: impl Middleware,
2910 mut cached_res: HttpResponse,
2911 mut policy: CachePolicy,
2912 ) -> Result<HttpResponse> {
2913 let parts = middleware.parts()?;
2914 let before_req = policy.before_request(&parts, SystemTime::now());
2915 match before_req {
2916 BeforeRequest::Fresh(parts) => {
2917 cached_res.update_headers(&parts)?;
2918 if self.options.cache_status_headers {
2919 cached_res.cache_status(HitOrMiss::HIT);
2920 cached_res.cache_lookup_status(HitOrMiss::HIT);
2921 }
2922 return Ok(cached_res);
2923 }
2924 BeforeRequest::Stale { request: parts, matches } => {
2925 if matches {
2926 middleware.update_headers(&parts)?;
2927 }
2928 }
2929 }
2930 let req_url = middleware.url()?;
2931 // Apply rate limiting before revalidation request
2932 self.apply_rate_limiting(&req_url).await;
2933 match middleware.remote_fetch().await {
2934 Ok(mut cond_res) => {
2935 let status = StatusCode::from_u16(cond_res.status)?;
2936 if status.is_server_error() && cached_res.must_revalidate() {
2937 // 111 Revalidation failed
2938 // MUST be included if a cache returns a stale response
2939 // because an attempt to revalidate the response failed,
2940 // due to an inability to reach the server.
2941 // (https://tools.ietf.org/html/rfc2616#section-14.46)
2942 cached_res.add_warning(
2943 &req_url,
2944 111,
2945 "Revalidation failed",
2946 );
2947 if self.options.cache_status_headers {
2948 cached_res.cache_status(HitOrMiss::HIT);
2949 }
2950 Ok(cached_res)
2951 } else if cond_res.status == 304 {
2952 let after_res = policy.after_response(
2953 &parts,
2954 &cond_res.parts()?,
2955 SystemTime::now(),
2956 );
2957 match after_res {
2958 AfterResponse::Modified(new_policy, parts)
2959 | AfterResponse::NotModified(new_policy, parts) => {
2960 policy = new_policy;
2961 cached_res.update_headers(&parts)?;
2962 }
2963 }
2964 if self.options.cache_status_headers {
2965 cached_res.cache_status(HitOrMiss::HIT);
2966 cached_res.cache_lookup_status(HitOrMiss::HIT);
2967 }
2968 self.options
2969 .modify_response_before_caching(&mut cached_res);
2970 let res = self
2971 .manager
2972 .put(
2973 self.options.create_cache_key(&parts, None),
2974 cached_res,
2975 policy,
2976 )
2977 .await?;
2978 Ok(res)
2979 } else if cond_res.status == 200 {
2980 let policy = match self.options.cache_options {
2981 Some(options) => middleware
2982 .policy_with_options(&cond_res, options)?,
2983 None => middleware.policy(&cond_res)?,
2984 };
2985 if self.options.cache_status_headers {
2986 cond_res.cache_status(HitOrMiss::MISS);
2987 cond_res.cache_lookup_status(HitOrMiss::HIT);
2988 }
2989 // Generate metadata using the provider callback if configured
2990 let response_parts = cond_res.parts()?;
2991 cond_res.metadata =
2992 self.options.generate_metadata(&parts, &response_parts);
2993
2994 self.options.modify_response_before_caching(&mut cond_res);
2995
2996 let mode = self.cache_mode(&middleware)?;
2997 // Apply response-based cache mode override if configured
2998 let mode = self
2999 .options
3000 .evaluate_response_cache_mode(&parts, &cond_res, mode);
3001 let is_get_head = middleware.is_method_get_head();
3002 let is_cacheable = self.options.should_cache_response(
3003 mode,
3004 &cond_res,
3005 is_get_head,
3006 &policy,
3007 );
3008
3009 if is_cacheable {
3010 let res = self
3011 .manager
3012 .put(
3013 self.options.create_cache_key(&parts, None),
3014 cond_res,
3015 policy,
3016 )
3017 .await?;
3018 Ok(res)
3019 } else {
3020 Ok(cond_res)
3021 }
3022 } else {
3023 // Return fresh response for any status other than 304 or 200
3024 if self.options.cache_status_headers {
3025 cond_res.cache_status(HitOrMiss::MISS);
3026 cond_res.cache_lookup_status(HitOrMiss::HIT);
3027 }
3028 Ok(cond_res)
3029 }
3030 }
3031 Err(e) => {
3032 if cached_res.must_revalidate() {
3033 Err(e)
3034 } else {
3035 // 111 Revalidation failed
3036 // MUST be included if a cache returns a stale response
3037 // because an attempt to revalidate the response failed,
3038 // due to an inability to reach the server.
3039 // (https://tools.ietf.org/html/rfc2616#section-14.46)
3040 cached_res.add_warning(
3041 &req_url,
3042 111,
3043 "Revalidation failed",
3044 );
3045 if self.options.cache_status_headers {
3046 cached_res.cache_status(HitOrMiss::HIT);
3047 }
3048 Ok(cached_res)
3049 }
3050 }
3051 }
3052 }
3053}
3054
3055impl<T: StreamingCacheManager> HttpCacheStreamInterface
3056 for HttpStreamingCache<T>
3057where
3058 <T::Body as http_body::Body>::Data: Send,
3059 <T::Body as http_body::Body>::Error:
3060 Into<StreamingError> + Send + Sync + 'static,
3061{
3062 type Body = T::Body;
3063
3064 fn analyze_request(
3065 &self,
3066 parts: &request::Parts,
3067 mode_override: Option<CacheMode>,
3068 ) -> Result<CacheAnalysis> {
3069 self.options.analyze_request_internal(parts, mode_override, self.mode)
3070 }
3071
3072 async fn lookup_cached_response(
3073 &self,
3074 key: &str,
3075 ) -> Result<Option<(Response<Self::Body>, CachePolicy)>> {
3076 self.manager.get(key).await
3077 }
3078
3079 async fn process_response<B>(
3080 &self,
3081 analysis: CacheAnalysis,
3082 response: Response<B>,
3083 metadata: Option<Vec<u8>>,
3084 ) -> Result<Response<Self::Body>>
3085 where
3086 B: http_body::Body + Send + 'static,
3087 B::Data: Send,
3088 B::Error: Into<StreamingError>,
3089 <T::Body as http_body::Body>::Data: Send,
3090 <T::Body as http_body::Body>::Error:
3091 Into<StreamingError> + Send + Sync + 'static,
3092 {
3093 // For non-cacheable requests based on initial analysis, convert them to manager's body type
3094 if !analysis.should_cache {
3095 // RFC 7234 s4.4: invalidate GET and HEAD cache for non-GET/HEAD on success/redirect
3096 if !analysis.is_get_head {
3097 let status = response.status();
3098 if status.is_success() || status.is_redirection() {
3099 self.manager
3100 .delete(&self.options.create_cache_key(
3101 &analysis.request_parts,
3102 Some("GET"),
3103 ))
3104 .await
3105 .ok();
3106 self.manager
3107 .delete(&self.options.create_cache_key(
3108 &analysis.request_parts,
3109 Some("HEAD"),
3110 ))
3111 .await
3112 .ok();
3113 }
3114 }
3115 let mut converted_response =
3116 self.manager.convert_body(response).await?;
3117 // Add cache miss headers
3118 if self.options.cache_status_headers {
3119 converted_response.headers_mut().insert(
3120 XCACHE,
3121 "MISS".parse().map_err(StreamingError::new)?,
3122 );
3123 converted_response.headers_mut().insert(
3124 XCACHELOOKUP,
3125 "MISS".parse().map_err(StreamingError::new)?,
3126 );
3127 }
3128 return Ok(converted_response);
3129 }
3130
3131 // Bust cache keys if needed
3132 for key in &analysis.cache_bust_keys {
3133 self.manager.delete(key).await?;
3134 }
3135
3136 // Convert response to HttpResponse format for response-based cache mode evaluation
3137 let (parts, body) = response.into_parts();
3138 // Use provided metadata or generate from provider
3139 let effective_metadata = metadata.or_else(|| {
3140 self.options.generate_metadata(&analysis.request_parts, &parts)
3141 });
3142 let http_response = self.options.parts_to_http_response(
3143 &parts,
3144 &analysis.request_parts,
3145 effective_metadata.clone(),
3146 )?;
3147
3148 // Check for response-based cache mode override
3149 let effective_cache_mode = self.options.evaluate_response_cache_mode(
3150 &analysis.request_parts,
3151 &http_response,
3152 analysis.cache_mode,
3153 );
3154
3155 // Reconstruct response for further processing
3156 let response = Response::from_parts(parts, body);
3157
3158 // If response-based override says NoStore, don't cache
3159 if effective_cache_mode == CacheMode::NoStore {
3160 // RFC 7234 s4.4: invalidate GET and HEAD cache for non-GET/HEAD on success/redirect
3161 if !analysis.is_get_head {
3162 let status_code = StatusCode::from_u16(http_response.status)?;
3163 if status_code.is_success() || status_code.is_redirection() {
3164 self.manager
3165 .delete(&self.options.create_cache_key(
3166 &analysis.request_parts,
3167 Some("GET"),
3168 ))
3169 .await
3170 .ok();
3171 self.manager
3172 .delete(&self.options.create_cache_key(
3173 &analysis.request_parts,
3174 Some("HEAD"),
3175 ))
3176 .await
3177 .ok();
3178 }
3179 }
3180 let mut converted_response =
3181 self.manager.convert_body(response).await?;
3182 // Add cache miss headers
3183 if self.options.cache_status_headers {
3184 converted_response.headers_mut().insert(
3185 XCACHE,
3186 "MISS".parse().map_err(StreamingError::new)?,
3187 );
3188 converted_response.headers_mut().insert(
3189 XCACHELOOKUP,
3190 "MISS".parse().map_err(StreamingError::new)?,
3191 );
3192 }
3193 return Ok(converted_response);
3194 }
3195
3196 // Create policy for the response
3197 let (parts, body) = response.into_parts();
3198 let policy =
3199 self.options.create_cache_policy(&analysis.request_parts, &parts);
3200
3201 // Reconstruct response for caching
3202 let response = Response::from_parts(parts, body);
3203
3204 let should_cache_response = self.options.should_cache_response(
3205 effective_cache_mode,
3206 &http_response,
3207 analysis.is_get_head,
3208 &policy,
3209 );
3210
3211 if should_cache_response {
3212 // Extract URL from request parts for caching
3213 let request_url =
3214 extract_url_from_request_parts(&analysis.request_parts)?;
3215
3216 // Apply modify_response_before_caching shim before storing
3217 let mut response = response;
3218 apply_modify_response_shim(
3219 &self.options,
3220 &mut response,
3221 &request_url,
3222 );
3223
3224 // Cache the response using the streaming manager
3225 let mut cached_response = self
3226 .manager
3227 .put(
3228 analysis.cache_key,
3229 response,
3230 policy,
3231 request_url,
3232 effective_metadata,
3233 )
3234 .await?;
3235
3236 // RFC 7234 s4.4: invalidate GET and HEAD cache for non-GET/HEAD on success/redirect
3237 if !analysis.is_get_head {
3238 let status_code = StatusCode::from_u16(http_response.status)?;
3239 if status_code.is_success() || status_code.is_redirection() {
3240 self.manager
3241 .delete(&self.options.create_cache_key(
3242 &analysis.request_parts,
3243 Some("GET"),
3244 ))
3245 .await
3246 .ok();
3247 self.manager
3248 .delete(&self.options.create_cache_key(
3249 &analysis.request_parts,
3250 Some("HEAD"),
3251 ))
3252 .await
3253 .ok();
3254 }
3255 }
3256
3257 // Add cache miss headers (response is being stored for first time)
3258 if self.options.cache_status_headers {
3259 cached_response.headers_mut().insert(
3260 XCACHE,
3261 "MISS".parse().map_err(StreamingError::new)?,
3262 );
3263 cached_response.headers_mut().insert(
3264 XCACHELOOKUP,
3265 "MISS".parse().map_err(StreamingError::new)?,
3266 );
3267 }
3268 Ok(cached_response)
3269 } else {
3270 // RFC 7234 s4.4: invalidate GET and HEAD cache for non-GET/HEAD on success/redirect
3271 if !analysis.is_get_head {
3272 let status_code = StatusCode::from_u16(http_response.status)?;
3273 if status_code.is_success() || status_code.is_redirection() {
3274 self.manager
3275 .delete(&self.options.create_cache_key(
3276 &analysis.request_parts,
3277 Some("GET"),
3278 ))
3279 .await
3280 .ok();
3281 self.manager
3282 .delete(&self.options.create_cache_key(
3283 &analysis.request_parts,
3284 Some("HEAD"),
3285 ))
3286 .await
3287 .ok();
3288 }
3289 }
3290 // Don't cache, just convert to manager's body type
3291 let mut converted_response =
3292 self.manager.convert_body(response).await?;
3293 // Add cache miss headers
3294 if self.options.cache_status_headers {
3295 converted_response.headers_mut().insert(
3296 XCACHE,
3297 "MISS".parse().map_err(StreamingError::new)?,
3298 );
3299 converted_response.headers_mut().insert(
3300 XCACHELOOKUP,
3301 "MISS".parse().map_err(StreamingError::new)?,
3302 );
3303 }
3304 Ok(converted_response)
3305 }
3306 }
3307
3308 fn prepare_conditional_request(
3309 &self,
3310 parts: &mut request::Parts,
3311 _cached_response: &Response<Self::Body>,
3312 policy: &CachePolicy,
3313 ) -> Result<()> {
3314 let before_req = policy.before_request(parts, SystemTime::now());
3315 if let BeforeRequest::Stale { request, .. } = before_req {
3316 parts.headers.extend(request.headers);
3317 }
3318 Ok(())
3319 }
3320
3321 async fn handle_not_modified(
3322 &self,
3323 cached_response: Response<Self::Body>,
3324 fresh_parts: &response::Parts,
3325 ) -> Result<Response<Self::Body>> {
3326 let (mut parts, body) = cached_response.into_parts();
3327
3328 // Update headers from the 304 response, preserving multi-valued
3329 // headers correctly. `HeaderMap::extend` from another `HeaderMap`
3330 // appends rather than replaces, so naive `extend` would double
3331 // same-named headers. Mirror the clear-then-append pattern used by
3332 // `HttpResponse::update_headers` and the streaming conditional-fetch
3333 // header merges.
3334 for name in fresh_parts.headers.keys() {
3335 parts.headers.remove(name);
3336 }
3337 for (name, value) in fresh_parts.headers.iter() {
3338 parts.headers.append(name.clone(), value.clone());
3339 }
3340
3341 let mut response = Response::from_parts(parts, body);
3342 if self.options.cache_status_headers {
3343 response_cache_status(&mut response, HitOrMiss::HIT);
3344 response_cache_lookup_status(&mut response, HitOrMiss::HIT);
3345 }
3346 Ok(response)
3347 }
3348}
3349
3350impl<T: CacheManager> HttpCacheInterface for HttpCache<T> {
3351 fn analyze_request(
3352 &self,
3353 parts: &request::Parts,
3354 mode_override: Option<CacheMode>,
3355 ) -> Result<CacheAnalysis> {
3356 self.options.analyze_request_internal(parts, mode_override, self.mode)
3357 }
3358
3359 async fn lookup_cached_response(
3360 &self,
3361 key: &str,
3362 ) -> Result<Option<(HttpResponse, CachePolicy)>> {
3363 self.manager.get(key).await
3364 }
3365
3366 async fn process_response(
3367 &self,
3368 analysis: CacheAnalysis,
3369 response: Response<Vec<u8>>,
3370 metadata: Option<Vec<u8>>,
3371 ) -> Result<Response<Vec<u8>>> {
3372 if !analysis.should_cache {
3373 // RFC 7234 s4.4: invalidate GET and HEAD cache for non-GET/HEAD on success/redirect
3374 if !analysis.is_get_head {
3375 let status = response.status();
3376 if status.is_success() || status.is_redirection() {
3377 self.manager
3378 .delete(&self.options.create_cache_key(
3379 &analysis.request_parts,
3380 Some("GET"),
3381 ))
3382 .await
3383 .ok();
3384 self.manager
3385 .delete(&self.options.create_cache_key(
3386 &analysis.request_parts,
3387 Some("HEAD"),
3388 ))
3389 .await
3390 .ok();
3391 }
3392 }
3393 return Ok(response);
3394 }
3395
3396 // Bust cache keys if needed
3397 for key in &analysis.cache_bust_keys {
3398 self.manager.delete(key).await?;
3399 }
3400
3401 // Convert response to HttpResponse format
3402 let (parts, body) = response.into_parts();
3403 // Use provided metadata or generate from provider
3404 let effective_metadata = metadata.or_else(|| {
3405 self.options.generate_metadata(&analysis.request_parts, &parts)
3406 });
3407 let mut http_response = self.options.parts_to_http_response(
3408 &parts,
3409 &analysis.request_parts,
3410 effective_metadata,
3411 )?;
3412 http_response.body = body.clone(); // Include the body for buffered cache managers
3413
3414 // Check for response-based cache mode override
3415 let effective_cache_mode = self.options.evaluate_response_cache_mode(
3416 &analysis.request_parts,
3417 &http_response,
3418 analysis.cache_mode,
3419 );
3420
3421 // If response-based override says NoStore, don't cache
3422 if effective_cache_mode == CacheMode::NoStore {
3423 // RFC 7234 s4.4: invalidate GET and HEAD cache for non-GET/HEAD on success/redirect
3424 if !analysis.is_get_head {
3425 let status = StatusCode::from_u16(http_response.status)?;
3426 if status.is_success() || status.is_redirection() {
3427 self.manager
3428 .delete(&self.options.create_cache_key(
3429 &analysis.request_parts,
3430 Some("GET"),
3431 ))
3432 .await
3433 .ok();
3434 self.manager
3435 .delete(&self.options.create_cache_key(
3436 &analysis.request_parts,
3437 Some("HEAD"),
3438 ))
3439 .await
3440 .ok();
3441 }
3442 }
3443 let response = Response::from_parts(parts, body);
3444 return Ok(response);
3445 }
3446
3447 // Create policy and determine if we should cache based on response-based mode
3448 let policy = self.options.create_cache_policy(
3449 &analysis.request_parts,
3450 &http_response.parts()?,
3451 );
3452
3453 let should_cache_response = self.options.should_cache_response(
3454 effective_cache_mode,
3455 &http_response,
3456 analysis.is_get_head,
3457 &policy,
3458 );
3459
3460 if should_cache_response {
3461 self.options.modify_response_before_caching(&mut http_response);
3462 let cached_response = self
3463 .manager
3464 .put(analysis.cache_key, http_response, policy)
3465 .await?;
3466
3467 // RFC 7234 s4.4: invalidate GET and HEAD cache for non-GET/HEAD on success/redirect
3468 if !analysis.is_get_head {
3469 let status = StatusCode::from_u16(cached_response.status)?;
3470 if status.is_success() || status.is_redirection() {
3471 self.manager
3472 .delete(&self.options.create_cache_key(
3473 &analysis.request_parts,
3474 Some("GET"),
3475 ))
3476 .await
3477 .ok();
3478 self.manager
3479 .delete(&self.options.create_cache_key(
3480 &analysis.request_parts,
3481 Some("HEAD"),
3482 ))
3483 .await
3484 .ok();
3485 }
3486 }
3487
3488 // Convert back to standard Response
3489 let response_parts = cached_response.parts()?;
3490 let mut response = Response::builder()
3491 .status(response_parts.status)
3492 .version(response_parts.version)
3493 .body(cached_response.body)?;
3494
3495 // Copy headers from the response parts
3496 *response.headers_mut() = response_parts.headers;
3497
3498 Ok(response)
3499 } else {
3500 // RFC 7234 s4.4: invalidate GET and HEAD cache for non-GET/HEAD on success/redirect
3501 if !analysis.is_get_head {
3502 let status = StatusCode::from_u16(http_response.status)?;
3503 if status.is_success() || status.is_redirection() {
3504 self.manager
3505 .delete(&self.options.create_cache_key(
3506 &analysis.request_parts,
3507 Some("GET"),
3508 ))
3509 .await
3510 .ok();
3511 self.manager
3512 .delete(&self.options.create_cache_key(
3513 &analysis.request_parts,
3514 Some("HEAD"),
3515 ))
3516 .await
3517 .ok();
3518 }
3519 }
3520 // Don't cache, return original response
3521 let response = Response::from_parts(parts, body);
3522 Ok(response)
3523 }
3524 }
3525
3526 fn prepare_conditional_request(
3527 &self,
3528 parts: &mut request::Parts,
3529 _cached_response: &HttpResponse,
3530 policy: &CachePolicy,
3531 ) -> Result<()> {
3532 let before_req = policy.before_request(parts, SystemTime::now());
3533 if let BeforeRequest::Stale { request, .. } = before_req {
3534 parts.headers.extend(request.headers);
3535 }
3536 Ok(())
3537 }
3538
3539 async fn handle_not_modified(
3540 &self,
3541 mut cached_response: HttpResponse,
3542 fresh_parts: &response::Parts,
3543 ) -> Result<HttpResponse> {
3544 cached_response.update_headers(fresh_parts)?;
3545 if self.options.cache_status_headers {
3546 cached_response.cache_status(HitOrMiss::HIT);
3547 cached_response.cache_lookup_status(HitOrMiss::HIT);
3548 }
3549 Ok(cached_response)
3550 }
3551}
3552
3553#[cfg(test)]
3554mod test;