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;