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