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;