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;