Skip to main content

http_cache_reqwest/
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//! # http-cache-reqwest
16//!
17//! HTTP caching middleware for the [reqwest] HTTP client.
18//!
19//! This middleware implements HTTP caching according to RFC 7234 for the reqwest HTTP client library.
20//! It works as part of the [reqwest-middleware] ecosystem to provide caching capabilities.
21//!
22//! ```no_run
23//! # #[cfg(feature = "reqwest-middleware")]
24//! use reqwest::Client;
25//! # #[cfg(feature = "reqwest-middleware")]
26//! use reqwest_middleware::ClientBuilder;
27//! # #[cfg(feature = "reqwest-middleware")]
28//! use http_cache_reqwest::{Cache, CacheMode, RedbManager, HttpCache, HttpCacheOptions};
29//!
30//! # #[cfg(feature = "reqwest-middleware")]
31//! #[tokio::main]
32//! async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
33//!     let client = ClientBuilder::new(Client::new())
34//!         .with(Cache(HttpCache {
35//!             mode: CacheMode::Default,
36//!             manager: RedbManager::new("./http-cache.redb")?,
37//!             options: HttpCacheOptions::default(),
38//!         }))
39//!         .build();
40//!     
41//!     // This request will be cached according to response headers
42//!     let response = client
43//!         .get("https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching")
44//!         .send()
45//!         .await?;
46//!     println!("Status: {}", response.status());
47//!     
48//!     // Subsequent identical requests may be served from cache
49//!     let cached_response = client
50//!         .get("https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching")
51//!         .send()
52//!         .await?;
53//!     println!("Cached status: {}", cached_response.status());
54//!     
55//!     Ok(())
56//! }
57//! # #[cfg(not(feature = "reqwest-middleware"))]
58//! # fn main() {}
59//! ```
60//!
61//! ## Streaming Support
62//!
63//! The `StreamingCache` provides streaming support for large responses without buffering
64//! them entirely in memory. This is particularly useful for downloading large files or
65//! processing streaming APIs while still benefiting from HTTP caching.
66//!
67//! **Note**: Requires the `streaming` feature and a compatible cache manager that implements
68//! [`StreamingCacheManager`]. Currently only the `StreamingCacheManager` supports streaming -
69//! `CACacheManager` and `MokaManager` do not support streaming and will buffer responses
70//! in memory. The streaming implementation achieves significant memory savings
71//! (typically 35-40% reduction) compared to traditional buffered approaches.
72//!
73//! ```no_run
74//! # #[cfg(all(feature = "streaming", feature = "reqwest-middleware"))]
75//! use reqwest::Client;
76//! # #[cfg(all(feature = "streaming", feature = "reqwest-middleware"))]
77//! use reqwest_middleware::ClientBuilder;
78//! # #[cfg(all(feature = "streaming", feature = "reqwest-middleware"))]
79//! use http_cache_reqwest::{StreamingCache, CacheMode};
80//! # #[cfg(all(feature = "streaming", feature = "reqwest-middleware"))]
81//! use http_cache::StreamingManager;
82//!
83//! # #[cfg(all(feature = "streaming", feature = "reqwest-middleware"))]
84//! #[tokio::main]
85//! async fn main() -> reqwest_middleware::Result<()> {
86//!     let streaming_manager = StreamingManager::with_temp_dir(1000).await.unwrap();
87//!     let client = ClientBuilder::new(Client::new())
88//!         .with(StreamingCache::new(
89//!             streaming_manager,
90//!             CacheMode::Default,
91//!         ))
92//!         .build();
93//!         
94//!     // Stream large responses efficiently - cached responses are also streamed
95//!     let response = client
96//!         .get("https://httpbin.org/stream/1000")
97//!         .send()
98//!         .await?;
99//!     println!("Status: {}", response.status());
100//!     
101//!     // Process the streaming body chunk by chunk
102//!     use futures_util::StreamExt;
103//!     let mut stream = response.bytes_stream();
104//!     while let Some(chunk) = stream.next().await {
105//!         let chunk = chunk?;
106//!         println!("Received chunk of {} bytes", chunk.len());
107//!         // Process chunk without loading entire response into memory
108//!     }
109//!     
110//!     Ok(())
111//! }
112//! # #[cfg(not(all(feature = "streaming", feature = "reqwest-middleware")))]
113//! # fn main() {}
114//! ```
115//!
116//! ### Streaming Cache with Custom Options
117//!
118//! ```no_run
119//! # #[cfg(all(feature = "streaming", feature = "reqwest-middleware"))]
120//! use reqwest::Client;
121//! # #[cfg(all(feature = "streaming", feature = "reqwest-middleware"))]
122//! use reqwest_middleware::ClientBuilder;
123//! # #[cfg(all(feature = "streaming", feature = "reqwest-middleware"))]
124//! use http_cache_reqwest::{StreamingCache, CacheMode, HttpCacheOptions};
125//! # #[cfg(all(feature = "streaming", feature = "reqwest-middleware"))]
126//! use http_cache::StreamingManager;
127//!
128//! # #[cfg(all(feature = "streaming", feature = "reqwest-middleware"))]
129//! #[tokio::main]
130//! async fn main() -> reqwest_middleware::Result<()> {
131//!     let options = HttpCacheOptions {
132//!         cache_bust: Some(std::sync::Arc::new(|req: &http::request::Parts, _cache_key: &Option<std::sync::Arc<dyn Fn(&http::request::Parts) -> String + Send + Sync>>, _uri: &str| {
133//!             // Custom cache busting logic for streaming requests
134//!             if req.uri.path().contains("/stream/") {
135//!                 vec![format!("stream:{}", req.uri)]
136//!             } else {
137//!                 vec![]
138//!             }
139//!         })),
140//!         ..Default::default()
141//!     };
142//!
143//!     let streaming_manager = StreamingManager::with_temp_dir(1000).await.unwrap();
144//!     let client = ClientBuilder::new(Client::new())
145//!         .with(StreamingCache::with_options(
146//!             streaming_manager,
147//!             CacheMode::Default,
148//!             options,
149//!         ))
150//!         .build();
151//!         
152//!     Ok(())
153//! }
154//! # #[cfg(not(all(feature = "streaming", feature = "reqwest-middleware")))]
155//! # fn main() {}
156//! ```
157//!
158//! ## Cache Modes
159//!
160//! Control caching behavior with different modes:
161//!
162//! ```no_run
163//! # #[cfg(feature = "reqwest-middleware")]
164//! use reqwest::Client;
165//! # #[cfg(feature = "reqwest-middleware")]
166//! use reqwest_middleware::ClientBuilder;
167//! # #[cfg(feature = "reqwest-middleware")]
168//! use http_cache_reqwest::{Cache, CacheMode, RedbManager, HttpCache, HttpCacheOptions};
169//!
170//! # #[cfg(feature = "reqwest-middleware")]
171//! #[tokio::main]
172//! async fn main() -> reqwest_middleware::Result<()> {
173//!     let client = ClientBuilder::new(Client::new())
174//!         .with(Cache(HttpCache {
175//!             mode: CacheMode::ForceCache, // Cache everything, ignore headers
176//!             manager: RedbManager::new("./http-cache.redb").unwrap(),
177//!             options: HttpCacheOptions::default(),
178//!         }))
179//!         .build();
180//!     
181//!     // This will be cached even if headers say not to cache
182//!     client.get("https://httpbin.org/uuid").send().await?;
183//!     Ok(())
184//! }
185//! # #[cfg(not(feature = "reqwest-middleware"))]
186//! # fn main() {}
187//! ```
188//!
189//! ## Per-Request Cache Control
190//!
191//! Override the cache mode on individual requests:
192//!
193//! ```no_run
194//! # #[cfg(feature = "reqwest-middleware")]
195//! use reqwest::Client;
196//! # #[cfg(feature = "reqwest-middleware")]
197//! use reqwest_middleware::ClientBuilder;
198//! # #[cfg(feature = "reqwest-middleware")]
199//! use http_cache_reqwest::{Cache, CacheMode, RedbManager, HttpCache, HttpCacheOptions};
200//!
201//! # #[cfg(feature = "reqwest-middleware")]
202//! #[tokio::main]
203//! async fn main() -> reqwest_middleware::Result<()> {
204//!     let client = ClientBuilder::new(Client::new())
205//!         .with(Cache(HttpCache {
206//!             mode: CacheMode::Default,
207//!             manager: RedbManager::new("./http-cache.redb").unwrap(),
208//!             options: HttpCacheOptions::default(),
209//!         }))
210//!         .build();
211//!     
212//!     // Override cache mode for this specific request
213//!     let response = client.get("https://httpbin.org/uuid")
214//!         .with_extension(CacheMode::OnlyIfCached) // Only serve from cache
215//!         .send()
216//!         .await?;
217//!         
218//!     // This request bypasses cache completely
219//!     let fresh_response = client.get("https://httpbin.org/uuid")
220//!         .with_extension(CacheMode::NoStore)
221//!         .send()
222//!         .await?;
223//!         
224//!     Ok(())
225//! }
226//! # #[cfg(not(feature = "reqwest-middleware"))]
227//! # fn main() {}
228//! ```
229//!
230//! ## Custom Cache Keys
231//!
232//! Customize how cache keys are generated:
233//!
234//! ```no_run
235//! # #[cfg(feature = "reqwest-middleware")]
236//! use reqwest::Client;
237//! # #[cfg(feature = "reqwest-middleware")]
238//! use reqwest_middleware::ClientBuilder;
239//! # #[cfg(feature = "reqwest-middleware")]
240//! use http_cache_reqwest::{Cache, CacheMode, RedbManager, HttpCache, HttpCacheOptions};
241//! # #[cfg(feature = "reqwest-middleware")]
242//! use std::sync::Arc;
243//!
244//! # #[cfg(feature = "reqwest-middleware")]
245//! #[tokio::main]
246//! async fn main() -> reqwest_middleware::Result<()> {
247//!     let options = HttpCacheOptions {
248//!         cache_key: Some(Arc::new(|req: &http::request::Parts| {
249//!             // Include query parameters in cache key
250//!             format!("{}:{}", req.method, req.uri)
251//!         })),
252//!         ..Default::default()
253//!     };
254//!     
255//!     let client = ClientBuilder::new(Client::new())
256//!         .with(Cache(HttpCache {
257//!             mode: CacheMode::Default,
258//!             manager: RedbManager::new("./http-cache.redb").unwrap(),
259//!             options,
260//!         }))
261//!         .build();
262//!         
263//!     Ok(())
264//! }
265//! # #[cfg(not(feature = "reqwest-middleware"))]
266//! # fn main() {}
267//! ```
268//!
269//! ## In-Memory Caching
270//!
271//! Use the Moka in-memory cache:
272//!
273//! ```no_run
274//! # #[cfg(all(feature = "manager-moka", feature = "reqwest-middleware"))]
275//! use reqwest::Client;
276//! # #[cfg(all(feature = "manager-moka", feature = "reqwest-middleware"))]
277//! use reqwest_middleware::ClientBuilder;
278//! # #[cfg(all(feature = "manager-moka", feature = "reqwest-middleware"))]
279//! use http_cache_reqwest::{Cache, CacheMode, MokaManager, HttpCache, HttpCacheOptions};
280//! # #[cfg(all(feature = "manager-moka", feature = "reqwest-middleware"))]
281//! use http_cache_reqwest::MokaCache;
282//!
283//! # #[cfg(all(feature = "manager-moka", feature = "reqwest-middleware"))]
284//! #[tokio::main]
285//! async fn main() -> reqwest_middleware::Result<()> {
286//!     let client = ClientBuilder::new(Client::new())
287//!         .with(Cache(HttpCache {
288//!             mode: CacheMode::Default,
289//!             manager: MokaManager::new(MokaCache::new(1000)), // Max 1000 entries
290//!             options: HttpCacheOptions::default(),
291//!         }))
292//!         .build();
293//!         
294//!     Ok(())
295//! }
296//! # #[cfg(not(all(feature = "manager-moka", feature = "reqwest-middleware")))]
297//! # fn main() {}
298//! ```
299//!
300//! ## Redirect Caching
301//!
302//! The optional `middlewest` backend replaces the default `reqwest-middleware`,
303//! and with its `redirect` feature redirects are followed through the middleware
304//! chain, so each hop is cached under its own URL rather than the final response
305//! being cached under the original request URL:
306//!
307//! ```no_run
308//! # #[cfg(all(feature = "middlewest", feature = "redirect", feature = "manager-redb"))]
309//! use reqwest::Client;
310//! # #[cfg(all(feature = "middlewest", feature = "redirect", feature = "manager-redb"))]
311//! use middlewest::{ClientBuilder, redirect::RedirectPolicy};
312//! # #[cfg(all(feature = "middlewest", feature = "redirect", feature = "manager-redb"))]
313//! use http_cache_reqwest::{Cache, CacheMode, RedbManager, HttpCache, HttpCacheOptions};
314//!
315//! # #[cfg(all(feature = "middlewest", feature = "redirect", feature = "manager-redb"))]
316//! #[tokio::main]
317//! async fn main() -> middlewest::Result<()> {
318//!     let client = ClientBuilder::from_reqwest_builder(Client::builder())?
319//!         .with(Cache(HttpCache {
320//!             mode: CacheMode::Default,
321//!             manager: RedbManager::new("./http-cache.redb").unwrap(),
322//!             options: HttpCacheOptions::default(),
323//!         }))
324//!         .redirect(RedirectPolicy::limited(10))
325//!         .build();
326//!
327//!     client.get("https://httpbin.org/redirect/1").send().await?;
328//!     Ok(())
329//! }
330//! # #[cfg(not(all(feature = "middlewest", feature = "redirect", feature = "manager-redb")))]
331//! # fn main() {}
332//! ```
333// Re-export unified error types from http-cache core
334pub use http_cache::{BadRequest, HttpCacheError};
335
336#[cfg(feature = "streaming")]
337/// Type alias for reqwest streaming errors, using the unified streaming error system
338pub type ReqwestStreamingError = http_cache::ClientStreamingError;
339
340#[cfg(feature = "streaming")]
341use http_cache::StreamingCacheManager;
342
343use std::str::FromStr;
344
345pub use http::request::Parts;
346use http::{
347    header::{HeaderName, CACHE_CONTROL},
348    Extensions, HeaderValue, Method,
349};
350use http_cache::{
351    url_parse, BoxError, HitOrMiss, Middleware, Result, Url, XCACHE,
352    XCACHELOOKUP,
353};
354use reqwest::{Request, Response, ResponseBuilderExt};
355#[cfg(all(feature = "reqwest-middleware", feature = "middlewest"))]
356compile_error!(
357    "features `reqwest-middleware` and `middlewest` are mutually exclusive"
358);
359#[cfg(not(any(feature = "reqwest-middleware", feature = "middlewest")))]
360compile_error!(
361    "either feature `reqwest-middleware` or `middlewest` must be enabled"
362);
363
364use client_middleware::{Error, Next};
365#[cfg(feature = "middlewest")]
366pub(crate) use middlewest as client_middleware;
367#[cfg(not(feature = "middlewest"))]
368pub(crate) use reqwest_middleware as client_middleware;
369
370/// Helper function to convert our error types to reqwest middleware errors
371fn to_middleware_error<E: std::error::Error + Send + Sync + 'static>(
372    error: E,
373) -> Error {
374    // Convert to anyhow::Error which is what reqwest-middleware expects
375    Error::Middleware(anyhow::Error::new(error))
376}
377
378pub use http_cache::{
379    CacheManager, CacheMode, CacheOptions, HttpCache, HttpCacheMetadata,
380    HttpCacheOptions, HttpResponse, MetadataProvider, ResponseCacheModeFn,
381};
382
383#[cfg(feature = "streaming")]
384// Re-export streaming types for future use
385pub use http_cache::{
386    HttpCacheStreamInterface, HttpStreamingCache, StreamingBody,
387    StreamingManager,
388};
389
390#[cfg(feature = "manager-cacache")]
391#[cfg_attr(docsrs, doc(cfg(feature = "manager-cacache")))]
392pub use http_cache::CACacheManager;
393
394#[cfg(feature = "manager-redb")]
395#[cfg_attr(docsrs, doc(cfg(feature = "manager-redb")))]
396pub use http_cache::RedbManager;
397
398#[cfg(feature = "manager-moka")]
399#[cfg_attr(docsrs, doc(cfg(feature = "manager-moka")))]
400pub use http_cache::{MokaCache, MokaCacheBuilder, MokaManager};
401
402#[cfg(feature = "rate-limiting")]
403#[cfg_attr(docsrs, doc(cfg(feature = "rate-limiting")))]
404pub use http_cache::rate_limiting::{
405    CacheAwareRateLimiter, DirectRateLimiter, DomainRateLimiter, Quota,
406};
407
408/// Wrapper for [`HttpCache`]
409#[derive(Debug)]
410pub struct Cache<T: CacheManager>(pub HttpCache<T>);
411
412#[cfg(feature = "streaming")]
413/// Streaming cache wrapper that implements reqwest middleware for streaming responses
414#[derive(Debug, Clone)]
415pub struct StreamingCache<T: StreamingCacheManager> {
416    cache: HttpStreamingCache<T>,
417}
418
419#[cfg(feature = "streaming")]
420impl<T: StreamingCacheManager> StreamingCache<T> {
421    /// Create a new streaming cache with the given manager and mode
422    pub fn new(manager: T, mode: CacheMode) -> Self {
423        Self {
424            cache: HttpStreamingCache {
425                mode,
426                manager,
427                options: HttpCacheOptions::default(),
428            },
429        }
430    }
431
432    /// Create a new streaming cache with custom options
433    pub fn with_options(
434        manager: T,
435        mode: CacheMode,
436        options: HttpCacheOptions,
437    ) -> Self {
438        Self { cache: HttpStreamingCache { mode, manager, options } }
439    }
440}
441
442/// Implements ['Middleware'] for reqwest
443pub(crate) struct ReqwestMiddleware<'a> {
444    pub req: Request,
445    pub next: Next<'a>,
446    pub extensions: &'a mut Extensions,
447}
448
449fn clone_req(request: &Request) -> std::result::Result<Request, Error> {
450    match request.try_clone() {
451        Some(r) => Ok(r),
452        None => Err(to_middleware_error(BadRequest)),
453    }
454}
455
456impl Middleware for ReqwestMiddleware<'_> {
457    fn overridden_cache_mode(&self) -> Option<CacheMode> {
458        self.extensions.get().cloned()
459    }
460    fn is_method_get_head(&self) -> bool {
461        self.req.method() == Method::GET || self.req.method() == Method::HEAD
462    }
463    fn update_headers(&mut self, parts: &Parts) -> Result<()> {
464        for header in parts.headers.iter() {
465            self.req.headers_mut().insert(header.0.clone(), header.1.clone());
466        }
467        Ok(())
468    }
469    fn force_no_cache(&mut self) -> Result<()> {
470        self.req
471            .headers_mut()
472            .insert(CACHE_CONTROL, HeaderValue::from_str("no-cache")?);
473        Ok(())
474    }
475    fn parts(&self) -> Result<Parts> {
476        // Extract request parts without cloning the body
477        let mut builder = http::Request::builder()
478            .method(self.req.method().as_str())
479            .uri(self.req.url().as_str())
480            .version(self.req.version());
481
482        // Add headers
483        for (name, value) in self.req.headers() {
484            builder = builder.header(name, value);
485        }
486
487        // Add extensions
488        if let Some(no_error) = builder.extensions_mut() {
489            *no_error = self.extensions.clone();
490        }
491
492        // Build with empty body just to get the Parts
493        let http_req = builder.body(()).map_err(Box::new)?;
494        Ok(http_req.into_parts().0)
495    }
496    fn url(&self) -> Result<Url> {
497        // Re-parse the URL through our helper for url/ada-url compatibility
498        url_parse(self.req.url().as_str())
499    }
500    async fn remote_fetch(&mut self) -> Result<HttpResponse> {
501        let copied_req = clone_req(&self.req)?;
502        let res = self
503            .next
504            .clone()
505            .run(copied_req, self.extensions)
506            .await
507            .map_err(BoxError::from)?;
508        let headers = res.headers().into();
509        // Re-parse the URL through our helper for url/ada-url compatibility
510        let url = url_parse(res.url().as_str())?;
511        let status = res.status().into();
512        let version = res.version();
513        let body: Vec<u8> = res.bytes().await.map_err(BoxError::from)?.to_vec();
514        Ok(HttpResponse {
515            body,
516            headers,
517            status,
518            url,
519            version: version.try_into()?,
520            metadata: None,
521        })
522    }
523}
524
525// Converts an [`HttpResponse`] to a reqwest [`Response`]
526fn convert_response(response: HttpResponse) -> Result<Response> {
527    let metadata = response.metadata.clone();
528    // reqwest always uses url::Url internally, so we need to re-parse when using ada-url
529    let reqwest_url =
530        ::url::Url::parse(response.url.as_str()).map_err(BoxError::from)?;
531    let mut ret_res = http::Response::builder()
532        .status(response.status)
533        .url(reqwest_url)
534        .version(response.version.into())
535        .body(response.body)?;
536    for header in response.headers {
537        ret_res.headers_mut().append(
538            HeaderName::from_str(&header.0)?,
539            HeaderValue::from_str(&header.1)?,
540        );
541    }
542    // Insert metadata into response extensions if present
543    if let Some(metadata) = metadata {
544        ret_res.extensions_mut().insert(HttpCacheMetadata::from(metadata));
545    }
546    Ok(Response::from(ret_res))
547}
548
549#[cfg(feature = "streaming")]
550/// Final URL of the upstream response, carried through core's orchestrator
551/// in response extensions so the reqwest Response rebuilt on the way out
552/// reports the real URL instead of reqwest's no.url.provided.local
553/// placeholder.
554#[derive(Clone)]
555struct FinalUrl(::url::Url);
556
557#[cfg(feature = "streaming")]
558// Converts a reqwest Response into a genuinely streaming http::Response.
559// No body bytes are read here: reqwest::Body implements http_body::Body
560// (Data = Bytes, Error = reqwest::Error), so the network stream flows
561// through core's orchestrator and into the cache manager frame by frame.
562fn convert_reqwest_response_to_streaming(
563    response: Response,
564) -> http::Response<
565    http_body_util::combinators::UnsyncBoxBody<
566        bytes::Bytes,
567        http_cache::StreamingError,
568    >,
569> {
570    use http_body_util::BodyExt;
571    let url = response.url().clone();
572    let http_response: http::Response<reqwest::Body> = response.into();
573    let (mut parts, body) = http_response.into_parts();
574    parts.extensions.insert(FinalUrl(url));
575    let body = body.map_err(http_cache::StreamingError::client).boxed_unsync();
576    http::Response::from_parts(parts, body)
577}
578
579#[cfg(feature = "streaming")]
580// Converts a streaming response to reqwest Response using the StreamingCacheManager's method
581async fn convert_streaming_body_to_reqwest<T>(
582    response: http::Response<T::Body>,
583) -> Result<Response>
584where
585    T: StreamingCacheManager,
586    <T::Body as http_body::Body>::Data: Send,
587    <T::Body as http_body::Body>::Error: Send + Sync + 'static,
588{
589    let (mut parts, body) = response.into_parts();
590    let final_url = parts.extensions.remove::<FinalUrl>();
591
592    // Use the cache manager's body_to_bytes_stream method for streaming
593    let bytes_stream = T::body_to_bytes_stream(body);
594    let reqwest_body = reqwest::Body::wrap_stream(bytes_stream);
595
596    let mut builder =
597        http::Response::builder().status(parts.status).version(parts.version);
598    for (name, value) in parts.headers.iter() {
599        builder = builder.header(name, value);
600    }
601    // Transfer orchestrator extensions (HttpCacheMetadata etc.) into the
602    // builder BEFORE applying the URL, so the ResponseUrl the builder
603    // inserts is not clobbered.
604    if let Some(ext) = builder.extensions_mut() {
605        *ext = parts.extensions;
606    }
607    if let Some(FinalUrl(url)) = final_url {
608        builder = builder.url(url);
609    }
610    let response = builder.body(reqwest_body)?;
611    Ok(Response::from(response))
612}
613
614fn bad_header(e: reqwest::header::InvalidHeaderValue) -> Error {
615    to_middleware_error(HttpCacheError::Cache(e.to_string()))
616}
617
618fn from_box_error(e: BoxError) -> Error {
619    to_middleware_error(HttpCacheError::Cache(e.to_string()))
620}
621
622impl<T: CacheManager> Cache<T> {
623    async fn run(
624        &self,
625        req: Request,
626        extensions: &mut Extensions,
627        next: Next<'_>,
628    ) -> std::result::Result<Response, Error> {
629        let middleware = ReqwestMiddleware { req, next, extensions };
630        let can_cache =
631            self.0.can_cache_request(&middleware).map_err(from_box_error)?;
632
633        if can_cache {
634            let res = self.0.run(middleware).await.map_err(from_box_error)?;
635            let converted = convert_response(res).map_err(|e| {
636                to_middleware_error(HttpCacheError::Cache(e.to_string()))
637            })?;
638            Ok(converted)
639        } else {
640            let parts = middleware.parts().map_err(from_box_error)?;
641            let mut res = middleware
642                .next
643                .run(middleware.req, middleware.extensions)
644                .await?;
645
646            // Only invalidate for unsafe methods after successful response (RFC 7234 s4.4)
647            if !parts.method.is_safe()
648                && (res.status().is_success() || res.status().is_redirection())
649            {
650                self.0
651                    .run_no_cache_from_parts(&parts)
652                    .await
653                    .map_err(from_box_error)?;
654            }
655
656            if self.0.options.cache_status_headers {
657                let miss =
658                    HeaderValue::from_str(HitOrMiss::MISS.to_string().as_ref())
659                        .map_err(bad_header)?;
660                res.headers_mut().insert(XCACHE, miss.clone());
661                res.headers_mut().insert(XCACHELOOKUP, miss);
662            }
663            Ok(res)
664        }
665    }
666}
667
668#[cfg(feature = "reqwest-middleware")]
669#[async_trait::async_trait]
670impl<T: CacheManager> client_middleware::Middleware for Cache<T> {
671    async fn handle(
672        &self,
673        req: Request,
674        extensions: &mut Extensions,
675        next: Next<'_>,
676    ) -> std::result::Result<Response, Error> {
677        self.run(req, extensions, next).await
678    }
679}
680
681#[cfg(feature = "middlewest")]
682impl<T: CacheManager> client_middleware::Middleware for Cache<T> {
683    fn handle<'a>(
684        &'a self,
685        req: Request,
686        extensions: &'a mut Extensions,
687        next: Next<'a>,
688    ) -> client_middleware::BoxFuture<'a, std::result::Result<Response, Error>>
689    {
690        Box::pin(self.run(req, extensions, next))
691    }
692}
693
694#[cfg(feature = "streaming")]
695impl<T: StreamingCacheManager> StreamingCache<T>
696where
697    T::Body: Send + 'static,
698    <T::Body as http_body::Body>::Data: Send,
699    <T::Body as http_body::Body>::Error:
700        Into<http_cache::StreamingError> + Send + Sync + 'static,
701{
702    async fn run(
703        &self,
704        req: Request,
705        extensions: &mut Extensions,
706        next: Next<'_>,
707    ) -> std::result::Result<Response, Error> {
708        use http_cache::FetchRequest;
709
710        // Convert reqwest Request to http::Request for analysis.
711        // If the request can't be cloned (e.g., streaming body),
712        // bypass the cache gracefully.
713        let copied_req = match clone_req(&req) {
714            Ok(r) => r,
715            Err(_) => return next.run(req, extensions).await,
716        };
717        let http_req = http::Request::try_from(copied_req).map_err(|e| {
718            to_middleware_error(HttpCacheError::Cache(e.to_string()))
719        })?;
720        let (parts, _) = http_req.into_parts();
721        let mode_override = extensions.get::<CacheMode>().cloned();
722
723        let can_cache = self
724            .cache
725            .can_cache_request(&parts, mode_override)
726            .map_err(from_box_error)?;
727
728        if can_cache {
729            let mut result = self
730                .cache
731                .run(&parts, mode_override, |fetch_req| {
732                    let mut req = req;
733                    let next = next.clone();
734
735                    match fetch_req {
736                        FetchRequest::Fresh => {}
737                        FetchRequest::FreshNoCache => {
738                            req.headers_mut().insert(
739                                CACHE_CONTROL,
740                                HeaderValue::from_static("no-cache"),
741                            );
742                        }
743                        FetchRequest::Conditional(cond_parts) => {
744                            for (name, value) in cond_parts.headers.iter() {
745                                req.headers_mut()
746                                    .insert(name.clone(), value.clone());
747                            }
748                        }
749                    }
750
751                    async move {
752                        let resp = next.run(req, extensions).await.map_err(
753                            |e| -> BoxError { e.to_string().into() },
754                        )?;
755                        Ok(convert_reqwest_response_to_streaming(resp))
756                    }
757                })
758                .await
759                .map_err(from_box_error)?;
760
761            if result.extensions().get::<FinalUrl>().is_none() {
762                if let Ok(u) = ::url::Url::parse(&parts.uri.to_string()) {
763                    result.extensions_mut().insert(FinalUrl(u));
764                }
765            }
766
767            convert_streaming_body_to_reqwest::<T>(result).await.map_err(|e| {
768                to_middleware_error(HttpCacheError::Cache(e.to_string()))
769            })
770        } else {
771            let mut res = next.run(req, extensions).await?;
772
773            // Only invalidate for unsafe methods after successful response (RFC 7234 s4.4)
774            if !parts.method.is_safe()
775                && (res.status().is_success() || res.status().is_redirection())
776            {
777                self.cache
778                    .run_no_cache(&parts)
779                    .await
780                    .map_err(from_box_error)?;
781            }
782
783            if self.cache.options.cache_status_headers {
784                let miss =
785                    HeaderValue::from_str(HitOrMiss::MISS.to_string().as_ref())
786                        .map_err(bad_header)?;
787                res.headers_mut().insert(XCACHE, miss.clone());
788                res.headers_mut().insert(XCACHELOOKUP, miss);
789            }
790            Ok(res)
791        }
792    }
793}
794
795#[cfg(all(feature = "streaming", feature = "reqwest-middleware"))]
796#[async_trait::async_trait]
797impl<T: StreamingCacheManager> client_middleware::Middleware
798    for StreamingCache<T>
799where
800    T::Body: Send + 'static,
801    <T::Body as http_body::Body>::Data: Send,
802    <T::Body as http_body::Body>::Error:
803        Into<http_cache::StreamingError> + Send + Sync + 'static,
804{
805    async fn handle(
806        &self,
807        req: Request,
808        extensions: &mut Extensions,
809        next: Next<'_>,
810    ) -> std::result::Result<Response, Error> {
811        self.run(req, extensions, next).await
812    }
813}
814
815#[cfg(all(feature = "streaming", feature = "middlewest"))]
816impl<T: StreamingCacheManager> client_middleware::Middleware
817    for StreamingCache<T>
818where
819    T::Body: Send + 'static,
820    <T::Body as http_body::Body>::Data: Send,
821    <T::Body as http_body::Body>::Error:
822        Into<http_cache::StreamingError> + Send + Sync + 'static,
823{
824    fn handle<'a>(
825        &'a self,
826        req: Request,
827        extensions: &'a mut Extensions,
828        next: Next<'a>,
829    ) -> client_middleware::BoxFuture<'a, std::result::Result<Response, Error>>
830    {
831        Box::pin(self.run(req, extensions, next))
832    }
833}
834
835#[cfg(test)]
836mod test;