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, time::SystemTime};
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 http_cache_semantics::CachePolicy;
355use reqwest::{Request, Response, ResponseBuilderExt};
356#[cfg(all(feature = "reqwest-middleware", feature = "middlewest"))]
357compile_error!(
358    "features `reqwest-middleware` and `middlewest` are mutually exclusive"
359);
360#[cfg(not(any(feature = "reqwest-middleware", feature = "middlewest")))]
361compile_error!(
362    "either feature `reqwest-middleware` or `middlewest` must be enabled"
363);
364
365use client_middleware::{Error, Next};
366#[cfg(feature = "middlewest")]
367pub(crate) use middlewest as client_middleware;
368#[cfg(not(feature = "middlewest"))]
369pub(crate) use reqwest_middleware as client_middleware;
370
371/// Helper function to convert our error types to reqwest middleware errors
372fn to_middleware_error<E: std::error::Error + Send + Sync + 'static>(
373    error: E,
374) -> Error {
375    // Convert to anyhow::Error which is what reqwest-middleware expects
376    Error::Middleware(anyhow::Error::new(error))
377}
378
379pub use http_cache::{
380    CacheManager, CacheMode, CacheOptions, HttpCache, HttpCacheMetadata,
381    HttpCacheOptions, HttpResponse, MetadataProvider, ResponseCacheModeFn,
382};
383
384#[cfg(feature = "streaming")]
385// Re-export streaming types for future use
386pub use http_cache::{
387    HttpCacheStreamInterface, HttpStreamingCache, StreamingBody,
388    StreamingManager,
389};
390
391#[cfg(feature = "manager-cacache")]
392#[cfg_attr(docsrs, doc(cfg(feature = "manager-cacache")))]
393pub use http_cache::CACacheManager;
394
395#[cfg(feature = "manager-redb")]
396#[cfg_attr(docsrs, doc(cfg(feature = "manager-redb")))]
397pub use http_cache::RedbManager;
398
399#[cfg(feature = "manager-moka")]
400#[cfg_attr(docsrs, doc(cfg(feature = "manager-moka")))]
401pub use http_cache::{MokaCache, MokaCacheBuilder, MokaManager};
402
403#[cfg(feature = "rate-limiting")]
404#[cfg_attr(docsrs, doc(cfg(feature = "rate-limiting")))]
405pub use http_cache::rate_limiting::{
406    CacheAwareRateLimiter, DirectRateLimiter, DomainRateLimiter, Quota,
407};
408
409/// Wrapper for [`HttpCache`]
410#[derive(Debug)]
411pub struct Cache<T: CacheManager>(pub HttpCache<T>);
412
413#[cfg(feature = "streaming")]
414/// Streaming cache wrapper that implements reqwest middleware for streaming responses
415#[derive(Debug, Clone)]
416pub struct StreamingCache<T: StreamingCacheManager> {
417    cache: HttpStreamingCache<T>,
418}
419
420#[cfg(feature = "streaming")]
421impl<T: StreamingCacheManager> StreamingCache<T> {
422    /// Create a new streaming cache with the given manager and mode
423    pub fn new(manager: T, mode: CacheMode) -> Self {
424        Self {
425            cache: HttpStreamingCache {
426                mode,
427                manager,
428                options: HttpCacheOptions::default(),
429            },
430        }
431    }
432
433    /// Create a new streaming cache with custom options
434    pub fn with_options(
435        manager: T,
436        mode: CacheMode,
437        options: HttpCacheOptions,
438    ) -> Self {
439        Self { cache: HttpStreamingCache { mode, manager, options } }
440    }
441}
442
443/// Implements ['Middleware'] for reqwest
444pub(crate) struct ReqwestMiddleware<'a> {
445    pub req: Request,
446    pub next: Next<'a>,
447    pub extensions: &'a mut Extensions,
448}
449
450fn clone_req(request: &Request) -> std::result::Result<Request, Error> {
451    match request.try_clone() {
452        Some(r) => Ok(r),
453        None => Err(to_middleware_error(BadRequest)),
454    }
455}
456
457impl Middleware for ReqwestMiddleware<'_> {
458    fn overridden_cache_mode(&self) -> Option<CacheMode> {
459        self.extensions.get().cloned()
460    }
461    fn is_method_get_head(&self) -> bool {
462        self.req.method() == Method::GET || self.req.method() == Method::HEAD
463    }
464    fn policy(&self, response: &HttpResponse) -> Result<CachePolicy> {
465        Ok(CachePolicy::new(&self.parts()?, &response.parts()?))
466    }
467    fn policy_with_options(
468        &self,
469        response: &HttpResponse,
470        options: CacheOptions,
471    ) -> Result<CachePolicy> {
472        Ok(CachePolicy::new_options(
473            &self.parts()?,
474            &response.parts()?,
475            SystemTime::now(),
476            options,
477        ))
478    }
479    fn update_headers(&mut self, parts: &Parts) -> Result<()> {
480        for header in parts.headers.iter() {
481            self.req.headers_mut().insert(header.0.clone(), header.1.clone());
482        }
483        Ok(())
484    }
485    fn force_no_cache(&mut self) -> Result<()> {
486        self.req
487            .headers_mut()
488            .insert(CACHE_CONTROL, HeaderValue::from_str("no-cache")?);
489        Ok(())
490    }
491    fn parts(&self) -> Result<Parts> {
492        // Extract request parts without cloning the body
493        let mut builder = http::Request::builder()
494            .method(self.req.method().as_str())
495            .uri(self.req.url().as_str())
496            .version(self.req.version());
497
498        // Add headers
499        for (name, value) in self.req.headers() {
500            builder = builder.header(name, value);
501        }
502
503        // Add extensions
504        if let Some(no_error) = builder.extensions_mut() {
505            *no_error = self.extensions.clone();
506        }
507
508        // Build with empty body just to get the Parts
509        let http_req = builder.body(()).map_err(Box::new)?;
510        Ok(http_req.into_parts().0)
511    }
512    fn url(&self) -> Result<Url> {
513        // Re-parse the URL through our helper for url/ada-url compatibility
514        url_parse(self.req.url().as_str())
515    }
516    fn method(&self) -> Result<String> {
517        Ok(self.req.method().as_ref().to_string())
518    }
519    async fn remote_fetch(&mut self) -> Result<HttpResponse> {
520        let copied_req = clone_req(&self.req)?;
521        let res = self
522            .next
523            .clone()
524            .run(copied_req, self.extensions)
525            .await
526            .map_err(BoxError::from)?;
527        let headers = res.headers().into();
528        // Re-parse the URL through our helper for url/ada-url compatibility
529        let url = url_parse(res.url().as_str())?;
530        let status = res.status().into();
531        let version = res.version();
532        let body: Vec<u8> = res.bytes().await.map_err(BoxError::from)?.to_vec();
533        Ok(HttpResponse {
534            body,
535            headers,
536            status,
537            url,
538            version: version.try_into()?,
539            metadata: None,
540        })
541    }
542}
543
544// Converts an [`HttpResponse`] to a reqwest [`Response`]
545fn convert_response(response: HttpResponse) -> Result<Response> {
546    let metadata = response.metadata.clone();
547    // reqwest always uses url::Url internally, so we need to re-parse when using ada-url
548    let reqwest_url =
549        ::url::Url::parse(response.url.as_str()).map_err(BoxError::from)?;
550    let mut ret_res = http::Response::builder()
551        .status(response.status)
552        .url(reqwest_url)
553        .version(response.version.into())
554        .body(response.body)?;
555    for header in response.headers {
556        ret_res.headers_mut().append(
557            HeaderName::from_str(&header.0)?,
558            HeaderValue::from_str(&header.1)?,
559        );
560    }
561    // Insert metadata into response extensions if present
562    if let Some(metadata) = metadata {
563        ret_res.extensions_mut().insert(HttpCacheMetadata::from(metadata));
564    }
565    Ok(Response::from(ret_res))
566}
567
568#[cfg(feature = "streaming")]
569/// Final URL of the upstream response, carried through core's orchestrator
570/// in response extensions so the reqwest Response rebuilt on the way out
571/// reports the real URL instead of reqwest's no.url.provided.local
572/// placeholder.
573#[derive(Clone)]
574struct FinalUrl(::url::Url);
575
576#[cfg(feature = "streaming")]
577// Converts a reqwest Response into a genuinely streaming http::Response.
578// No body bytes are read here: reqwest::Body implements http_body::Body
579// (Data = Bytes, Error = reqwest::Error), so the network stream flows
580// through core's orchestrator and into the cache manager frame by frame.
581fn convert_reqwest_response_to_streaming(
582    response: Response,
583) -> http::Response<
584    http_body_util::combinators::UnsyncBoxBody<
585        bytes::Bytes,
586        http_cache::StreamingError,
587    >,
588> {
589    use http_body_util::BodyExt;
590    let url = response.url().clone();
591    let http_response: http::Response<reqwest::Body> = response.into();
592    let (mut parts, body) = http_response.into_parts();
593    parts.extensions.insert(FinalUrl(url));
594    let body = body.map_err(http_cache::StreamingError::client).boxed_unsync();
595    http::Response::from_parts(parts, body)
596}
597
598#[cfg(feature = "streaming")]
599// Converts a streaming response to reqwest Response using the StreamingCacheManager's method
600async fn convert_streaming_body_to_reqwest<T>(
601    response: http::Response<T::Body>,
602) -> Result<Response>
603where
604    T: StreamingCacheManager,
605    <T::Body as http_body::Body>::Data: Send,
606    <T::Body as http_body::Body>::Error: Send + Sync + 'static,
607{
608    let (mut parts, body) = response.into_parts();
609    let final_url = parts.extensions.remove::<FinalUrl>();
610
611    // Use the cache manager's body_to_bytes_stream method for streaming
612    let bytes_stream = T::body_to_bytes_stream(body);
613    let reqwest_body = reqwest::Body::wrap_stream(bytes_stream);
614
615    let mut builder =
616        http::Response::builder().status(parts.status).version(parts.version);
617    for (name, value) in parts.headers.iter() {
618        builder = builder.header(name, value);
619    }
620    // Transfer orchestrator extensions (HttpCacheMetadata etc.) into the
621    // builder BEFORE applying the URL, so the ResponseUrl the builder
622    // inserts is not clobbered.
623    if let Some(ext) = builder.extensions_mut() {
624        *ext = parts.extensions;
625    }
626    if let Some(FinalUrl(url)) = final_url {
627        builder = builder.url(url);
628    }
629    let response = builder.body(reqwest_body)?;
630    Ok(Response::from(response))
631}
632
633fn bad_header(e: reqwest::header::InvalidHeaderValue) -> Error {
634    to_middleware_error(HttpCacheError::Cache(e.to_string()))
635}
636
637fn from_box_error(e: BoxError) -> Error {
638    to_middleware_error(HttpCacheError::Cache(e.to_string()))
639}
640
641impl<T: CacheManager> Cache<T> {
642    async fn run(
643        &self,
644        req: Request,
645        extensions: &mut Extensions,
646        next: Next<'_>,
647    ) -> std::result::Result<Response, Error> {
648        let middleware = ReqwestMiddleware { req, next, extensions };
649        let can_cache =
650            self.0.can_cache_request(&middleware).map_err(from_box_error)?;
651
652        if can_cache {
653            let res = self.0.run(middleware).await.map_err(from_box_error)?;
654            let converted = convert_response(res).map_err(|e| {
655                to_middleware_error(HttpCacheError::Cache(e.to_string()))
656            })?;
657            Ok(converted)
658        } else {
659            let parts = middleware.parts().map_err(from_box_error)?;
660            let mut res = middleware
661                .next
662                .run(middleware.req, middleware.extensions)
663                .await?;
664
665            // Only invalidate for unsafe methods after successful response (RFC 7234 s4.4)
666            if !parts.method.is_safe()
667                && (res.status().is_success() || res.status().is_redirection())
668            {
669                self.0
670                    .run_no_cache_from_parts(&parts)
671                    .await
672                    .map_err(from_box_error)?;
673            }
674
675            if self.0.options.cache_status_headers {
676                let miss =
677                    HeaderValue::from_str(HitOrMiss::MISS.to_string().as_ref())
678                        .map_err(bad_header)?;
679                res.headers_mut().insert(XCACHE, miss.clone());
680                res.headers_mut().insert(XCACHELOOKUP, miss);
681            }
682            Ok(res)
683        }
684    }
685}
686
687#[cfg(feature = "reqwest-middleware")]
688#[async_trait::async_trait]
689impl<T: CacheManager> client_middleware::Middleware for Cache<T> {
690    async fn handle(
691        &self,
692        req: Request,
693        extensions: &mut Extensions,
694        next: Next<'_>,
695    ) -> std::result::Result<Response, Error> {
696        self.run(req, extensions, next).await
697    }
698}
699
700#[cfg(feature = "middlewest")]
701impl<T: CacheManager> client_middleware::Middleware for Cache<T> {
702    fn handle<'a>(
703        &'a self,
704        req: Request,
705        extensions: &'a mut Extensions,
706        next: Next<'a>,
707    ) -> client_middleware::BoxFuture<'a, std::result::Result<Response, Error>>
708    {
709        Box::pin(self.run(req, extensions, next))
710    }
711}
712
713#[cfg(feature = "streaming")]
714impl<T: StreamingCacheManager> StreamingCache<T>
715where
716    T::Body: Send + 'static,
717    <T::Body as http_body::Body>::Data: Send,
718    <T::Body as http_body::Body>::Error:
719        Into<http_cache::StreamingError> + Send + Sync + 'static,
720{
721    async fn run(
722        &self,
723        req: Request,
724        extensions: &mut Extensions,
725        next: Next<'_>,
726    ) -> std::result::Result<Response, Error> {
727        use http_cache::FetchRequest;
728
729        // Convert reqwest Request to http::Request for analysis.
730        // If the request can't be cloned (e.g., streaming body),
731        // bypass the cache gracefully.
732        let copied_req = match clone_req(&req) {
733            Ok(r) => r,
734            Err(_) => return next.run(req, extensions).await,
735        };
736        let http_req = http::Request::try_from(copied_req).map_err(|e| {
737            to_middleware_error(HttpCacheError::Cache(e.to_string()))
738        })?;
739        let (parts, _) = http_req.into_parts();
740        let mode_override = extensions.get::<CacheMode>().cloned();
741
742        let can_cache = self
743            .cache
744            .can_cache_request(&parts, mode_override)
745            .map_err(from_box_error)?;
746
747        if can_cache {
748            let mut result = self
749                .cache
750                .run(&parts, mode_override, |fetch_req| {
751                    let mut req = req;
752                    let next = next.clone();
753
754                    match fetch_req {
755                        FetchRequest::Fresh => {}
756                        FetchRequest::FreshNoCache => {
757                            req.headers_mut().insert(
758                                CACHE_CONTROL,
759                                HeaderValue::from_static("no-cache"),
760                            );
761                        }
762                        FetchRequest::Conditional(cond_parts) => {
763                            for (name, value) in cond_parts.headers.iter() {
764                                req.headers_mut()
765                                    .insert(name.clone(), value.clone());
766                            }
767                        }
768                    }
769
770                    async move {
771                        let resp = next.run(req, extensions).await.map_err(
772                            |e| -> BoxError { e.to_string().into() },
773                        )?;
774                        Ok(convert_reqwest_response_to_streaming(resp))
775                    }
776                })
777                .await
778                .map_err(from_box_error)?;
779
780            if result.extensions().get::<FinalUrl>().is_none() {
781                if let Ok(u) = ::url::Url::parse(&parts.uri.to_string()) {
782                    result.extensions_mut().insert(FinalUrl(u));
783                }
784            }
785
786            convert_streaming_body_to_reqwest::<T>(result).await.map_err(|e| {
787                to_middleware_error(HttpCacheError::Cache(e.to_string()))
788            })
789        } else {
790            let mut res = next.run(req, extensions).await?;
791
792            // Only invalidate for unsafe methods after successful response (RFC 7234 s4.4)
793            if !parts.method.is_safe()
794                && (res.status().is_success() || res.status().is_redirection())
795            {
796                self.cache
797                    .run_no_cache(&parts)
798                    .await
799                    .map_err(from_box_error)?;
800            }
801
802            if self.cache.options.cache_status_headers {
803                let miss =
804                    HeaderValue::from_str(HitOrMiss::MISS.to_string().as_ref())
805                        .map_err(bad_header)?;
806                res.headers_mut().insert(XCACHE, miss.clone());
807                res.headers_mut().insert(XCACHELOOKUP, miss);
808            }
809            Ok(res)
810        }
811    }
812}
813
814#[cfg(all(feature = "streaming", feature = "reqwest-middleware"))]
815#[async_trait::async_trait]
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    async fn handle(
825        &self,
826        req: Request,
827        extensions: &mut Extensions,
828        next: Next<'_>,
829    ) -> std::result::Result<Response, Error> {
830        self.run(req, extensions, next).await
831    }
832}
833
834#[cfg(all(feature = "streaming", feature = "middlewest"))]
835impl<T: StreamingCacheManager> client_middleware::Middleware
836    for StreamingCache<T>
837where
838    T::Body: Send + 'static,
839    <T::Body as http_body::Body>::Data: Send,
840    <T::Body as http_body::Body>::Error:
841        Into<http_cache::StreamingError> + Send + Sync + 'static,
842{
843    fn handle<'a>(
844        &'a self,
845        req: Request,
846        extensions: &'a mut Extensions,
847        next: Next<'a>,
848    ) -> client_middleware::BoxFuture<'a, std::result::Result<Response, Error>>
849    {
850        Box::pin(self.run(req, extensions, next))
851    }
852}
853
854#[cfg(test)]
855mod test;