http_cache_surf/
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 caching middleware for the surf HTTP client.
16//!
17//! This crate provides middleware for the surf HTTP client that implements HTTP caching
18//! according to RFC 7234. It supports various cache modes and storage backends.
19//!
20//! ## Basic Usage
21//!
22//! Add HTTP caching to your surf client:
23//!
24//! ```no_run
25//! use surf::Client;
26//! use http_cache_surf::{Cache, CACacheManager, HttpCache, CacheMode};
27//! use macro_rules_attribute::apply;
28//! use smol_macros::main;
29//!
30//! #[apply(main!)]
31//! async fn main() -> surf::Result<()> {
32//!     let client = surf::Client::new()
33//!         .with(Cache(HttpCache {
34//!             mode: CacheMode::Default,
35//!             manager: CACacheManager::new("./cache".into(), true),
36//!             options: Default::default(),
37//!         }));
38//!
39//!     // This request will be cached according to response headers
40//!     let mut res = client.get("https://httpbin.org/cache/60").await?;
41//!     println!("Response: {}", res.body_string().await?);
42//!     
43//!     // Subsequent identical requests may be served from cache
44//!     let mut cached_res = client.get("https://httpbin.org/cache/60").await?;
45//!     println!("Cached response: {}", cached_res.body_string().await?);
46//!     
47//!     Ok(())
48//! }
49//! ```
50//!
51//! ## Cache Modes
52//!
53//! Control caching behavior with different modes:
54//!
55//! ```no_run
56//! use surf::Client;
57//! use http_cache_surf::{Cache, CACacheManager, HttpCache, CacheMode};
58//! use macro_rules_attribute::apply;
59//! use smol_macros::main;
60//!
61//! #[apply(main!)]
62//! async fn main() -> surf::Result<()> {
63//!     let client = surf::Client::new()
64//!         .with(Cache(HttpCache {
65//!             mode: CacheMode::ForceCache, // Cache everything, ignore headers
66//!             manager: CACacheManager::new("./cache".into(), true),
67//!             options: Default::default(),
68//!         }));
69//!
70//!     // This will be cached even if headers say not to cache
71//!     let mut res = client.get("https://httpbin.org/uuid").await?;
72//!     println!("{}", res.body_string().await?);
73//!     Ok(())
74//! }
75//! ```
76//!
77//! ## In-Memory Caching
78//!
79//! Use the Moka in-memory cache:
80//!
81//! ```no_run
82//! # #[cfg(feature = "manager-moka")]
83//! use surf::Client;
84//! # #[cfg(feature = "manager-moka")]
85//! use http_cache_surf::{Cache, MokaManager, HttpCache, CacheMode};
86//! # #[cfg(feature = "manager-moka")]
87//! use http_cache_surf::MokaCache;
88//! # #[cfg(feature = "manager-moka")]
89//! use macro_rules_attribute::apply;
90//! # #[cfg(feature = "manager-moka")]
91//! use smol_macros::main;
92//!
93//! # #[cfg(feature = "manager-moka")]
94//! #[apply(main!)]
95//! async fn main() -> surf::Result<()> {
96//!     let client = surf::Client::new()
97//!         .with(Cache(HttpCache {
98//!             mode: CacheMode::Default,
99//!             manager: MokaManager::new(MokaCache::new(1000)), // Max 1000 entries
100//!             options: Default::default(),
101//!         }));
102//!
103//!     let mut res = client.get("https://httpbin.org/cache/60").await?;
104//!     println!("{}", res.body_string().await?);
105//!     Ok(())
106//! }
107//! # #[cfg(not(feature = "manager-moka"))]
108//! # fn main() {}
109//! ```
110//!
111//! ## Custom Cache Keys
112//!
113//! Customize how cache keys are generated:
114//!
115//! ```no_run
116//! use surf::Client;
117//! use http_cache_surf::{Cache, CACacheManager, HttpCache, CacheMode};
118//! use http_cache::HttpCacheOptions;
119//! use std::sync::Arc;
120//! use macro_rules_attribute::apply;
121//! use smol_macros::main;
122//!
123//! #[apply(main!)]
124//! async fn main() -> surf::Result<()> {
125//!     let options = HttpCacheOptions {
126//!         cache_key: Some(Arc::new(|parts: &http::request::Parts| {
127//!             // Include query parameters in cache key
128//!             format!("{}:{}", parts.method, parts.uri)
129//!         })),
130//!         ..Default::default()
131//!     };
132//!     
133//!     let client = surf::Client::new()
134//!         .with(Cache(HttpCache {
135//!             mode: CacheMode::Default,
136//!             manager: CACacheManager::new("./cache".into(), true),
137//!             options,
138//!         }));
139//!
140//!     let mut res = client.get("https://httpbin.org/cache/60?param=value").await?;
141//!     println!("{}", res.body_string().await?);
142//!     Ok(())
143//! }
144//! ```
145
146use std::convert::TryInto;
147use std::str::FromStr;
148use std::time::SystemTime;
149
150use http::{
151    header::CACHE_CONTROL,
152    request::{self, Parts},
153};
154use http_cache::{
155    BadHeader, BoxError, CacheManager, CacheOptions, HitOrMiss, HttpResponse,
156    Middleware, Result, XCACHE, XCACHELOOKUP,
157};
158pub use http_cache::{CacheMode, HttpCache, HttpHeaders};
159use http_cache_semantics::CachePolicy;
160use http_types::{
161    headers::HeaderValue as HttpTypesHeaderValue,
162    Response as HttpTypesResponse, StatusCode as HttpTypesStatusCode,
163    Version as HttpTypesVersion,
164};
165use http_types::{Method as HttpTypesMethod, Request, Url};
166use surf::{middleware::Next, Client};
167
168// Re-export managers and cache types
169#[cfg(feature = "manager-cacache")]
170pub use http_cache::CACacheManager;
171
172pub use http_cache::HttpCacheOptions;
173pub use http_cache::ResponseCacheModeFn;
174
175#[cfg(feature = "manager-moka")]
176#[cfg_attr(docsrs, doc(cfg(feature = "manager-moka")))]
177pub use http_cache::{MokaCache, MokaCacheBuilder, MokaManager};
178
179#[cfg(feature = "rate-limiting")]
180#[cfg_attr(docsrs, doc(cfg(feature = "rate-limiting")))]
181pub use http_cache::rate_limiting::{
182    CacheAwareRateLimiter, DirectRateLimiter, DomainRateLimiter, Quota,
183};
184
185/// A wrapper around [`HttpCache`] that implements [`surf::middleware::Middleware`]
186#[derive(Debug, Clone)]
187pub struct Cache<T: CacheManager>(pub HttpCache<T>);
188
189// Re-export unified error types from http-cache core
190pub use http_cache::{BadRequest, HttpCacheError};
191
192/// Implements ['Middleware'] for surf
193pub(crate) struct SurfMiddleware<'a> {
194    pub req: Request,
195    pub client: Client,
196    pub next: Next<'a>,
197}
198
199#[async_trait::async_trait]
200impl Middleware for SurfMiddleware<'_> {
201    fn is_method_get_head(&self) -> bool {
202        self.req.method() == HttpTypesMethod::Get
203            || self.req.method() == HttpTypesMethod::Head
204    }
205    fn policy(&self, response: &HttpResponse) -> Result<CachePolicy> {
206        Ok(CachePolicy::new(&self.parts()?, &response.parts()?))
207    }
208    fn policy_with_options(
209        &self,
210        response: &HttpResponse,
211        options: CacheOptions,
212    ) -> Result<CachePolicy> {
213        Ok(CachePolicy::new_options(
214            &self.parts()?,
215            &response.parts()?,
216            SystemTime::now(),
217            options,
218        ))
219    }
220    fn update_headers(&mut self, parts: &Parts) -> Result<()> {
221        for header in parts.headers.iter() {
222            let value = match HttpTypesHeaderValue::from_str(header.1.to_str()?)
223            {
224                Ok(v) => v,
225                Err(_e) => return Err(Box::new(BadHeader)),
226            };
227            self.req.insert_header(header.0.as_str(), value);
228        }
229        Ok(())
230    }
231    fn force_no_cache(&mut self) -> Result<()> {
232        self.req.insert_header(CACHE_CONTROL.as_str(), "no-cache");
233        Ok(())
234    }
235    fn parts(&self) -> Result<Parts> {
236        let mut converted = request::Builder::new()
237            .method(self.req.method().as_ref())
238            .uri(self.req.url().as_str())
239            .body(())?;
240        {
241            let headers = converted.headers_mut();
242            for header in self.req.iter() {
243                headers.insert(
244                    http::header::HeaderName::from_str(header.0.as_str())?,
245                    http::HeaderValue::from_str(header.1.as_str())?,
246                );
247            }
248        }
249        Ok(converted.into_parts().0)
250    }
251    fn url(&self) -> Result<Url> {
252        Ok(self.req.url().clone())
253    }
254    fn method(&self) -> Result<String> {
255        Ok(self.req.method().as_ref().to_string())
256    }
257    async fn remote_fetch(&mut self) -> Result<HttpResponse> {
258        let url = self.req.url().clone();
259        let mut res =
260            self.next.run(self.req.clone().into(), self.client.clone()).await?;
261        let mut headers = HttpHeaders::new();
262        for header in res.iter() {
263            headers.insert(
264                header.0.as_str().to_owned(),
265                header.1.as_str().to_owned(),
266            );
267        }
268        let status = res.status().into();
269        let version = res.version().unwrap_or(HttpTypesVersion::Http1_1);
270        let body: Vec<u8> = res.body_bytes().await?;
271        Ok(HttpResponse {
272            body,
273            headers,
274            status,
275            url,
276            version: version.try_into()?,
277            metadata: None,
278        })
279    }
280}
281
282fn to_http_types_error(e: BoxError) -> http_types::Error {
283    http_types::Error::from_str(500, format!("HTTP cache error: {e}"))
284}
285
286#[surf::utils::async_trait]
287impl<T: CacheManager> surf::middleware::Middleware for Cache<T> {
288    async fn handle(
289        &self,
290        req: surf::Request,
291        client: Client,
292        next: Next<'_>,
293    ) -> std::result::Result<surf::Response, http_types::Error> {
294        let req: Request = req.into();
295        let mut middleware = SurfMiddleware { req, client, next };
296        if self.0.can_cache_request(&middleware).map_err(to_http_types_error)? {
297            let res =
298                self.0.run(middleware).await.map_err(to_http_types_error)?;
299            let mut converted = HttpTypesResponse::new(HttpTypesStatusCode::Ok);
300            for header in &res.headers {
301                let val = HttpTypesHeaderValue::from_bytes(
302                    header.1.as_bytes().to_vec(),
303                )?;
304                converted.insert_header(header.0.as_str(), val);
305            }
306            converted.set_status(res.status.try_into()?);
307            converted.set_version(Some(res.version.into()));
308            converted.set_body(res.body);
309            Ok(surf::Response::from(converted))
310        } else {
311            self.0
312                .run_no_cache(&mut middleware)
313                .await
314                .map_err(to_http_types_error)?;
315            let mut res = middleware
316                .next
317                .run(middleware.req.into(), middleware.client)
318                .await?;
319            let miss = HitOrMiss::MISS.to_string();
320            res.append_header(XCACHE, miss.clone());
321            res.append_header(XCACHELOOKUP, miss);
322            Ok(res)
323        }
324    }
325}
326
327#[cfg(test)]
328mod test;