http_cache_ureq/
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-ureq
16//!
17//! HTTP caching wrapper for the [ureq] HTTP client.
18//!
19//! This crate provides a caching wrapper around the ureq HTTP client that implements
20//! HTTP caching according to RFC 7234. Since ureq is a synchronous HTTP client, this
21//! wrapper uses the [smol] async runtime to integrate with the async http-cache system.
22//!
23//! ## Features
24//!
25//! - `json` - Enables JSON request/response support via `send_json()` and `into_json()` methods (requires `serde_json`)
26//! - `manager-cacache` - Enable [cacache](https://docs.rs/cacache/) cache manager (default)
27//! - `manager-moka` - Enable [moka](https://docs.rs/moka/) cache manager
28//!
29//! ## Basic Usage
30//!
31//! ```no_run
32//! use http_cache_ureq::{CachedAgent, CACacheManager, CacheMode};
33//!
34//! fn main() -> Result<(), Box<dyn std::error::Error>> {
35//!     smol::block_on(async {
36//!         let agent = CachedAgent::builder()
37//!             .cache_manager(CACacheManager::new("./cache".into(), true))
38//!             .cache_mode(CacheMode::Default)
39//!             .build()?;
40//!         
41//!         // This request will be cached according to response headers
42//!         let response = agent.get("https://httpbin.org/cache/60").call().await?;
43//!         println!("Status: {}", response.status());
44//!         println!("Cached: {}", response.is_cached());
45//!         println!("Response: {}", response.into_string()?);
46//!         
47//!         // Subsequent identical requests may be served from cache
48//!         let cached_response = agent.get("https://httpbin.org/cache/60").call().await?;
49//!         println!("Cached status: {}", cached_response.status());
50//!         println!("Is cached: {}", cached_response.is_cached());
51//!         println!("Cached response: {}", cached_response.into_string()?);
52//!         
53//!         Ok(())
54//!     })
55//! }
56//! ```
57//!
58//! ## Cache Modes
59//!
60//! Control caching behavior with different modes:
61//!
62//! ```no_run
63//! use http_cache_ureq::{CachedAgent, CACacheManager, CacheMode};
64//!
65//! fn main() -> Result<(), Box<dyn std::error::Error>> {
66//!     smol::block_on(async {
67//!         let agent = CachedAgent::builder()
68//!             .cache_manager(CACacheManager::new("./cache".into(), true))
69//!             .cache_mode(CacheMode::ForceCache) // Cache everything, ignore headers
70//!             .build()?;
71//!         
72//!         // This will be cached even if headers say not to cache
73//!         let response = agent.get("https://httpbin.org/uuid").call().await?;
74//!         println!("Response: {}", response.into_string()?);
75//!         
76//!         Ok(())
77//!     })
78//! }
79//! ```
80//!
81//! ## JSON Support
82//!
83//! Enable the `json` feature to send and parse JSON data:
84//!
85//! ```no_run
86//! # #[cfg(feature = "json")]
87//! use http_cache_ureq::{CachedAgent, CACacheManager, CacheMode};
88//! # #[cfg(feature = "json")]
89//! use serde_json::json;
90//!
91//! # #[cfg(feature = "json")]
92//! fn main() -> Result<(), Box<dyn std::error::Error>> {
93//!     smol::block_on(async {
94//!         let agent = CachedAgent::builder()
95//!             .cache_manager(CACacheManager::new("./cache".into(), true))
96//!             .cache_mode(CacheMode::Default)
97//!             .build()?;
98//!         
99//!         // Send JSON data
100//!         let response = agent.post("https://httpbin.org/post")
101//!             .send_json(json!({"key": "value"}))
102//!             .await?;
103//!         
104//!         // Parse JSON response
105//!         let json: serde_json::Value = response.into_json()?;
106//!         println!("Response: {}", json);
107//!         
108//!         Ok(())
109//!     })
110//! }
111//! # #[cfg(not(feature = "json"))]
112//! # fn main() {}
113//! ```
114//!
115//! ## In-Memory Caching
116//!
117//! Use the Moka in-memory cache:
118//!
119//! ```no_run
120//! # #[cfg(feature = "manager-moka")]
121//! use http_cache_ureq::{CachedAgent, MokaManager, MokaCache, CacheMode};
122//! # #[cfg(feature = "manager-moka")]
123//!
124//! # #[cfg(feature = "manager-moka")]
125//! fn main() -> Result<(), Box<dyn std::error::Error>> {
126//!     smol::block_on(async {
127//!         let agent = CachedAgent::builder()
128//!             .cache_manager(MokaManager::new(MokaCache::new(1000))) // Max 1000 entries
129//!             .cache_mode(CacheMode::Default)
130//!             .build()?;
131//!             
132//!         let response = agent.get("https://httpbin.org/cache/60").call().await?;
133//!         println!("Response: {}", response.into_string()?);
134//!         
135//!         Ok(())
136//!     })
137//! }
138//! # #[cfg(not(feature = "manager-moka"))]
139//! # fn main() {}
140//! ```
141//!
142//! ## Custom Cache Keys
143//!
144//! Customize how cache keys are generated:
145//!
146//! ```no_run
147//! use http_cache_ureq::{CachedAgent, CACacheManager, CacheMode, HttpCacheOptions};
148//! use std::sync::Arc;
149//!
150//! fn main() -> Result<(), Box<dyn std::error::Error>> {
151//!     smol::block_on(async {
152//!     let options = HttpCacheOptions {
153//!         cache_key: Some(Arc::new(|parts: &http::request::Parts| {
154//!             // Include query parameters in cache key
155//!             format!("{}:{}", parts.method, parts.uri)
156//!         })),
157//!         ..Default::default()
158//!     };
159//!     
160//!     let agent = CachedAgent::builder()
161//!         .cache_manager(CACacheManager::new("./cache".into(), true))
162//!         .cache_mode(CacheMode::Default)
163//!         .cache_options(options)
164//!         .build()?;
165//!         
166//!     let response = agent.get("https://httpbin.org/cache/60?param=value").call().await?;
167//!     println!("Response: {}", response.into_string()?);
168//!     
169//!         Ok(())
170//!     })
171//! }
172//! ```
173//!
174//! ## Maximum TTL Control
175//!
176//! Set a maximum time-to-live for cached responses, particularly useful with `CacheMode::IgnoreRules`:
177//!
178//! ```no_run
179//! use http_cache_ureq::{CachedAgent, CACacheManager, CacheMode, HttpCacheOptions};
180//! use std::time::Duration;
181//!
182//! fn main() -> Result<(), Box<dyn std::error::Error>> {
183//!     smol::block_on(async {
184//!         let agent = CachedAgent::builder()
185//!             .cache_manager(CACacheManager::new("./cache".into(), true))
186//!             .cache_mode(CacheMode::IgnoreRules) // Ignore server cache-control headers
187//!             .cache_options(HttpCacheOptions {
188//!                 max_ttl: Some(Duration::from_secs(300)), // Limit cache to 5 minutes regardless of server headers
189//!                 ..Default::default()
190//!             })
191//!             .build()?;
192//!         
193//!         // This will be cached for max 5 minutes even if server says cache longer
194//!         let response = agent.get("https://httpbin.org/cache/3600").call().await?;
195//!         println!("Response: {}", response.into_string()?);
196//!         
197//!         Ok(())
198//!     })
199//! }
200//! ```
201
202// Re-export unified error types from http-cache core
203pub use http_cache::{BadRequest, HttpCacheError};
204
205use std::{
206    collections::HashMap, result::Result, str::FromStr, time::SystemTime,
207};
208
209use async_trait::async_trait;
210
211pub use http::request::Parts;
212use http::{header::CACHE_CONTROL, Method};
213use http_cache::{
214    BoxError, CacheManager, CacheOptions, HitOrMiss, HttpResponse, Middleware,
215    XCACHE, XCACHELOOKUP,
216};
217use http_cache_semantics::CachePolicy;
218use url::Url;
219
220pub use http_cache::{
221    CacheMode, HttpCache, HttpCacheOptions, ResponseCacheModeFn,
222};
223
224#[cfg(feature = "manager-cacache")]
225#[cfg_attr(docsrs, doc(cfg(feature = "manager-cacache")))]
226pub use http_cache::CACacheManager;
227
228#[cfg(feature = "manager-moka")]
229#[cfg_attr(docsrs, doc(cfg(feature = "manager-moka")))]
230pub use http_cache::{MokaCache, MokaCacheBuilder, MokaManager};
231
232#[cfg(feature = "rate-limiting")]
233#[cfg_attr(docsrs, doc(cfg(feature = "rate-limiting")))]
234pub use http_cache::rate_limiting::{
235    CacheAwareRateLimiter, DirectRateLimiter, DomainRateLimiter, Quota,
236};
237
238/// A cached HTTP agent that wraps ureq with HTTP caching capabilities
239#[derive(Debug, Clone)]
240pub struct CachedAgent<T: CacheManager> {
241    agent: ureq::Agent,
242    cache: HttpCache<T>,
243}
244
245/// Builder for creating a CachedAgent
246#[derive(Debug)]
247pub struct CachedAgentBuilder<T: CacheManager> {
248    agent_config: Option<ureq::config::Config>,
249    cache_manager: Option<T>,
250    cache_mode: CacheMode,
251    cache_options: HttpCacheOptions,
252}
253
254impl<T: CacheManager> Default for CachedAgentBuilder<T> {
255    fn default() -> Self {
256        Self {
257            agent_config: None,
258            cache_manager: None,
259            cache_mode: CacheMode::Default,
260            cache_options: HttpCacheOptions::default(),
261        }
262    }
263}
264
265impl<T: CacheManager> CachedAgentBuilder<T> {
266    /// Create a new builder
267    pub fn new() -> Self {
268        Self::default()
269    }
270
271    /// Set the ureq agent configuration
272    ///
273    /// The provided configuration will be used to preserve your settings like
274    /// timeout, proxy, TLS config, and user agent. However, `http_status_as_error`
275    /// will always be set to `false` to ensure proper cache operation.
276    ///
277    /// This is necessary because the cache middleware needs to see all HTTP responses
278    /// (including 4xx and 5xx status codes) to make proper caching decisions.
279    pub fn agent_config(mut self, config: ureq::config::Config) -> Self {
280        self.agent_config = Some(config);
281        self
282    }
283
284    /// Set the cache manager
285    pub fn cache_manager(mut self, manager: T) -> Self {
286        self.cache_manager = Some(manager);
287        self
288    }
289
290    /// Set the cache mode
291    pub fn cache_mode(mut self, mode: CacheMode) -> Self {
292        self.cache_mode = mode;
293        self
294    }
295
296    /// Set cache options
297    pub fn cache_options(mut self, options: HttpCacheOptions) -> Self {
298        self.cache_options = options;
299        self
300    }
301
302    /// Build the cached agent
303    pub fn build(self) -> Result<CachedAgent<T>, HttpCacheError> {
304        let agent = if let Some(user_config) = self.agent_config {
305            // Extract user preferences and rebuild with cache-compatible settings
306            let mut config_builder =
307                ureq::config::Config::builder().http_status_as_error(false); // Force this to false for cache compatibility
308
309            // Preserve user's timeout settings
310            let timeouts = user_config.timeouts();
311            if timeouts.global.is_some()
312                || timeouts.connect.is_some()
313                || timeouts.send_request.is_some()
314            {
315                if let Some(global) = timeouts.global {
316                    config_builder =
317                        config_builder.timeout_global(Some(global));
318                }
319                if let Some(connect) = timeouts.connect {
320                    config_builder =
321                        config_builder.timeout_connect(Some(connect));
322                }
323                if let Some(send_request) = timeouts.send_request {
324                    config_builder =
325                        config_builder.timeout_send_request(Some(send_request));
326                }
327            }
328
329            // Preserve user's proxy setting
330            if let Some(proxy) = user_config.proxy() {
331                config_builder = config_builder.proxy(Some(proxy.clone()));
332            }
333
334            // Preserve user's TLS config
335            let tls_config = user_config.tls_config();
336            config_builder = config_builder.tls_config(tls_config.clone());
337
338            // Preserve user's user agent
339            let user_agent = user_config.user_agent();
340            config_builder = config_builder.user_agent(user_agent.clone());
341
342            let config = config_builder.build();
343            ureq::Agent::new_with_config(config)
344        } else {
345            // Create default config with http_status_as_error disabled
346            let config = ureq::config::Config::builder()
347                .http_status_as_error(false)
348                .build();
349            ureq::Agent::new_with_config(config)
350        };
351
352        let cache_manager = self.cache_manager.ok_or_else(|| {
353            HttpCacheError::Cache("Cache manager is required".to_string())
354        })?;
355
356        Ok(CachedAgent {
357            agent,
358            cache: HttpCache {
359                mode: self.cache_mode,
360                manager: cache_manager,
361                options: self.cache_options,
362            },
363        })
364    }
365}
366
367impl<T: CacheManager> CachedAgent<T> {
368    /// Create a new builder
369    pub fn builder() -> CachedAgentBuilder<T> {
370        CachedAgentBuilder::new()
371    }
372
373    /// Create a GET request
374    pub fn get(&self, url: &str) -> CachedRequestBuilder<'_, T> {
375        CachedRequestBuilder {
376            agent: self,
377            method: "GET".to_string(),
378            url: url.to_string(),
379            headers: Vec::new(),
380        }
381    }
382
383    /// Create a POST request  
384    pub fn post(&self, url: &str) -> CachedRequestBuilder<'_, T> {
385        CachedRequestBuilder {
386            agent: self,
387            method: "POST".to_string(),
388            url: url.to_string(),
389            headers: Vec::new(),
390        }
391    }
392
393    /// Create a PUT request
394    pub fn put(&self, url: &str) -> CachedRequestBuilder<'_, T> {
395        CachedRequestBuilder {
396            agent: self,
397            method: "PUT".to_string(),
398            url: url.to_string(),
399            headers: Vec::new(),
400        }
401    }
402
403    /// Create a DELETE request
404    pub fn delete(&self, url: &str) -> CachedRequestBuilder<'_, T> {
405        CachedRequestBuilder {
406            agent: self,
407            method: "DELETE".to_string(),
408            url: url.to_string(),
409            headers: Vec::new(),
410        }
411    }
412
413    /// Create a HEAD request
414    pub fn head(&self, url: &str) -> CachedRequestBuilder<'_, T> {
415        CachedRequestBuilder {
416            agent: self,
417            method: "HEAD".to_string(),
418            url: url.to_string(),
419            headers: Vec::new(),
420        }
421    }
422
423    /// Create a request with a custom method
424    pub fn request(
425        &self,
426        method: &str,
427        url: &str,
428    ) -> CachedRequestBuilder<'_, T> {
429        CachedRequestBuilder {
430            agent: self,
431            method: method.to_string(),
432            url: url.to_string(),
433            headers: Vec::new(),
434        }
435    }
436}
437
438/// A cached HTTP request builder that integrates ureq requests with HTTP caching
439#[derive(Debug)]
440pub struct CachedRequestBuilder<'a, T: CacheManager> {
441    agent: &'a CachedAgent<T>,
442    method: String,
443    url: String,
444    headers: Vec<(String, String)>,
445}
446
447impl<'a, T: CacheManager> CachedRequestBuilder<'a, T> {
448    /// Add a header to the request
449    pub fn set(mut self, header: &str, value: &str) -> Self {
450        self.headers.push((header.to_string(), value.to_string()));
451        self
452    }
453
454    /// Send JSON data with the request
455    #[cfg(feature = "json")]
456    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
457    pub async fn send_json(
458        self,
459        data: serde_json::Value,
460    ) -> Result<CachedResponse, HttpCacheError> {
461        let agent = self.agent.agent.clone();
462        let url = self.url.clone();
463        let method = self.method;
464        let headers = self.headers.clone();
465        let url_for_response = url.clone();
466
467        let response = smol::unblock(move || {
468            execute_json_request(&agent, &method, &url, &headers, data).map_err(
469                |e| {
470                    HttpCacheError::http(Box::new(std::io::Error::other(
471                        e.to_string(),
472                    )))
473                },
474            )
475        })
476        .await?;
477
478        let cached = smol::unblock(move || {
479            Ok::<_, HttpCacheError>(CachedResponse::from_ureq_response(
480                response,
481                &url_for_response,
482            ))
483        })
484        .await?;
485
486        Ok(cached)
487    }
488
489    /// Send string data with the request
490    pub async fn send_string(
491        self,
492        data: &str,
493    ) -> Result<CachedResponse, HttpCacheError> {
494        let data = data.to_string();
495        let agent = self.agent.agent.clone();
496        let url = self.url.clone();
497        let method = self.method;
498        let headers = self.headers.clone();
499        let url_for_response = url.clone();
500
501        let response = smol::unblock(move || {
502            execute_request(&agent, &method, &url, &headers, Some(&data))
503                .map_err(|e| {
504                    HttpCacheError::http(Box::new(std::io::Error::other(
505                        e.to_string(),
506                    )))
507                })
508        })
509        .await?;
510
511        let cached = smol::unblock(move || {
512            Ok::<_, HttpCacheError>(CachedResponse::from_ureq_response(
513                response,
514                &url_for_response,
515            ))
516        })
517        .await?;
518
519        Ok(cached)
520    }
521
522    /// Execute the request with caching
523    pub async fn call(self) -> Result<CachedResponse, HttpCacheError> {
524        let mut middleware = UreqMiddleware {
525            method: self.method.to_string(),
526            url: self.url.clone(),
527            headers: self.headers.clone(),
528            agent: &self.agent.agent,
529        };
530
531        // Check if we can cache this request
532        if self
533            .agent
534            .cache
535            .can_cache_request(&middleware)
536            .map_err(|e| HttpCacheError::Cache(e.to_string()))?
537        {
538            // Use the cache system
539            let response = self
540                .agent
541                .cache
542                .run(middleware)
543                .await
544                .map_err(|e| HttpCacheError::Cache(e.to_string()))?;
545
546            Ok(CachedResponse::from(response))
547        } else {
548            // Execute without cache but add cache headers
549            self.agent
550                .cache
551                .run_no_cache(&mut middleware)
552                .await
553                .map_err(|e| HttpCacheError::Cache(e.to_string()))?;
554
555            // Execute the request directly
556            let agent = self.agent.agent.clone();
557            let url = self.url.clone();
558            let method = self.method;
559            let headers = self.headers.clone();
560            let url_for_response = url.clone();
561            let cache_status_headers =
562                self.agent.cache.options.cache_status_headers;
563
564            let response = smol::unblock(move || {
565                execute_request(&agent, &method, &url, &headers, None).map_err(
566                    |e| {
567                        HttpCacheError::http(Box::new(std::io::Error::other(
568                            e.to_string(),
569                        )))
570                    },
571                )
572            })
573            .await?;
574
575            let mut cached_response = smol::unblock(move || {
576                Ok::<_, HttpCacheError>(CachedResponse::from_ureq_response(
577                    response,
578                    &url_for_response,
579                ))
580            })
581            .await?;
582
583            // Add cache status headers if enabled
584            if cache_status_headers {
585                cached_response
586                    .headers
587                    .insert(XCACHE.to_string(), HitOrMiss::MISS.to_string());
588                cached_response.headers.insert(
589                    XCACHELOOKUP.to_string(),
590                    HitOrMiss::MISS.to_string(),
591                );
592            }
593
594            Ok(cached_response)
595        }
596    }
597}
598
599/// Middleware implementation for ureq integration
600struct UreqMiddleware<'a> {
601    method: String,
602    url: String,
603    headers: Vec<(String, String)>,
604    agent: &'a ureq::Agent,
605}
606
607fn is_cacheable_method(method: &str) -> bool {
608    matches!(method, "GET" | "HEAD")
609}
610
611/// Universal function to execute HTTP requests - replaces all method-specific duplication
612fn execute_request(
613    agent: &ureq::Agent,
614    method: &str,
615    url: &str,
616    headers: &[(String, String)],
617    body: Option<&str>,
618) -> Result<http::Response<ureq::Body>, ureq::Error> {
619    // Build http::Request directly - eliminates all method-specific switching
620    let mut http_request = http::Request::builder().method(method).uri(url);
621
622    // Add headers
623    for (name, value) in headers {
624        http_request = http_request.header(name, value);
625    }
626
627    // Build request with or without body
628    let request = match body {
629        Some(data) => http_request.body(data.as_bytes().to_vec()),
630        None => http_request.body(Vec::new()),
631    }
632    .map_err(|e| ureq::Error::BadUri(e.to_string()))?;
633
634    // Use ureq's universal run method - this replaces ALL the method-specific logic
635    agent.run(request)
636}
637
638#[cfg(feature = "json")]
639/// Universal function for JSON requests - eliminates method-specific duplication
640fn execute_json_request(
641    agent: &ureq::Agent,
642    method: &str,
643    url: &str,
644    headers: &[(String, String)],
645    data: serde_json::Value,
646) -> Result<http::Response<ureq::Body>, ureq::Error> {
647    let json_string = serde_json::to_string(&data).map_err(|e| {
648        ureq::Error::Io(std::io::Error::new(
649            std::io::ErrorKind::InvalidData,
650            format!("JSON serialization error: {}", e),
651        ))
652    })?;
653
654    // Just call the universal execute_request with JSON body
655    let mut json_headers = headers.to_vec();
656    json_headers
657        .push(("Content-Type".to_string(), "application/json".to_string()));
658
659    execute_request(agent, method, url, &json_headers, Some(&json_string))
660}
661
662fn convert_ureq_response_to_http_response(
663    mut response: http::Response<ureq::Body>,
664    url: &str,
665) -> Result<HttpResponse, HttpCacheError> {
666    let status = response.status();
667    let mut headers = HashMap::new();
668
669    // Copy headers
670    for (name, value) in response.headers() {
671        let value_str = value.to_str().map_err(|e| {
672            HttpCacheError::http(Box::new(std::io::Error::other(format!(
673                "Invalid header value: {}",
674                e
675            ))))
676        })?;
677        headers.insert(name.as_str().to_string(), value_str.to_string());
678    }
679
680    // Read body using read_to_string
681    let body_string = response.body_mut().read_to_string().map_err(|e| {
682        HttpCacheError::http(Box::new(std::io::Error::other(format!(
683            "Failed to read response body: {}",
684            e
685        ))))
686    })?;
687
688    let body = body_string.into_bytes();
689
690    // Parse the provided URL
691    let parsed_url = Url::parse(url).map_err(|e| {
692        HttpCacheError::http(Box::new(std::io::Error::other(format!(
693            "Invalid URL '{}': {}",
694            url, e
695        ))))
696    })?;
697
698    Ok(HttpResponse {
699        body,
700        headers,
701        status: status.as_u16(),
702        url: parsed_url,
703        version: http_cache::HttpVersion::Http11,
704    })
705}
706
707/// A response wrapper that can represent both cached and fresh responses
708#[derive(Debug)]
709pub struct CachedResponse {
710    status: u16,
711    headers: HashMap<String, String>,
712    body: Vec<u8>,
713    url: String,
714    cached: bool,
715}
716
717impl CachedResponse {
718    /// Get the response status code
719    pub fn status(&self) -> u16 {
720        self.status
721    }
722
723    /// Get the response URL
724    pub fn url(&self) -> &str {
725        &self.url
726    }
727
728    /// Get a header value
729    pub fn header(&self, name: &str) -> Option<&str> {
730        self.headers.get(name).map(|s| s.as_str())
731    }
732
733    /// Get all header names
734    pub fn headers_names(&self) -> impl Iterator<Item = &String> {
735        self.headers.keys()
736    }
737
738    /// Check if this response came from cache
739    pub fn is_cached(&self) -> bool {
740        self.cached
741    }
742
743    /// Convert the response body to a string
744    pub fn into_string(self) -> Result<String, HttpCacheError> {
745        String::from_utf8(self.body).map_err(|e| {
746            HttpCacheError::http(Box::new(std::io::Error::other(format!(
747                "Invalid UTF-8 in response body: {}",
748                e
749            ))))
750        })
751    }
752
753    /// Get the response body as bytes
754    pub fn as_bytes(&self) -> &[u8] {
755        &self.body
756    }
757
758    /// Convert to bytes, consuming the response
759    pub fn into_bytes(self) -> Vec<u8> {
760        self.body
761    }
762
763    /// Parse response body as JSON
764    #[cfg(feature = "json")]
765    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
766    pub fn into_json<T: serde::de::DeserializeOwned>(
767        self,
768    ) -> Result<T, HttpCacheError> {
769        serde_json::from_slice(&self.body).map_err(|e| {
770            HttpCacheError::http(Box::new(std::io::Error::other(format!(
771                "JSON parse error: {}",
772                e
773            ))))
774        })
775    }
776}
777
778impl CachedResponse {
779    /// Create a CachedResponse from a ureq response with a known URL
780    fn from_ureq_response(
781        mut response: http::Response<ureq::Body>,
782        url: &str,
783    ) -> Self {
784        let status = response.status().as_u16();
785
786        let mut headers = HashMap::new();
787        for (name, value) in response.headers() {
788            let value_str = value.to_str().unwrap_or("");
789            headers.insert(name.as_str().to_string(), value_str.to_string());
790        }
791
792        // Note: Cache headers will be added by the cache system based on cache_status_headers option
793        // Don't add them here unconditionally
794
795        // Read the body
796        let body = if let Ok(body_string) = response.body_mut().read_to_string()
797        {
798            body_string.into_bytes()
799        } else {
800            Vec::new()
801        };
802
803        Self { status, headers, body, url: url.to_string(), cached: false }
804    }
805}
806
807impl From<HttpResponse> for CachedResponse {
808    fn from(response: HttpResponse) -> Self {
809        // Cache headers should already be added by the cache system
810        // based on the cache_status_headers option, so don't add them here
811        Self {
812            status: response.status,
813            headers: response.headers,
814            body: response.body,
815            url: response.url.to_string(),
816            cached: true,
817        }
818    }
819}
820
821#[async_trait]
822impl Middleware for UreqMiddleware<'_> {
823    fn is_method_get_head(&self) -> bool {
824        is_cacheable_method(&self.method)
825    }
826
827    fn policy(
828        &self,
829        response: &HttpResponse,
830    ) -> http_cache::Result<CachePolicy> {
831        let parts = self.build_http_parts()?;
832        Ok(CachePolicy::new(&parts, &response.parts()?))
833    }
834
835    fn policy_with_options(
836        &self,
837        response: &HttpResponse,
838        options: CacheOptions,
839    ) -> http_cache::Result<CachePolicy> {
840        let parts = self.build_http_parts()?;
841        Ok(CachePolicy::new_options(
842            &parts,
843            &response.parts()?,
844            SystemTime::now(),
845            options,
846        ))
847    }
848
849    fn update_headers(&mut self, parts: &Parts) -> http_cache::Result<()> {
850        for (name, value) in parts.headers.iter() {
851            let value_str = value.to_str().map_err(|e| {
852                BoxError::from(format!("Invalid header value: {}", e))
853            })?;
854            self.headers
855                .push((name.as_str().to_string(), value_str.to_string()));
856        }
857        Ok(())
858    }
859
860    fn force_no_cache(&mut self) -> http_cache::Result<()> {
861        self.headers
862            .push((CACHE_CONTROL.as_str().to_string(), "no-cache".to_string()));
863        Ok(())
864    }
865
866    fn parts(&self) -> http_cache::Result<Parts> {
867        self.build_http_parts()
868    }
869
870    fn url(&self) -> http_cache::Result<Url> {
871        Url::parse(&self.url).map_err(BoxError::from)
872    }
873
874    fn method(&self) -> http_cache::Result<String> {
875        Ok(self.method.clone())
876    }
877
878    async fn remote_fetch(&mut self) -> http_cache::Result<HttpResponse> {
879        let agent = self.agent.clone();
880        let method = self.method.clone();
881        let url = self.url.clone();
882        let headers = self.headers.clone();
883
884        let url_for_conversion = url.clone();
885        let response = smol::unblock(move || {
886            execute_request(&agent, &method, &url, &headers, None)
887                .map_err(|e| e.to_string())
888        })
889        .await
890        .map_err(BoxError::from)?;
891
892        // Convert the blocking response and read body on a blocking thread
893        let http_response = smol::unblock(move || {
894            convert_ureq_response_to_http_response(
895                response,
896                &url_for_conversion,
897            )
898            .map_err(|e| e.to_string())
899        })
900        .await
901        .map_err(BoxError::from)?;
902
903        Ok(http_response)
904    }
905}
906
907impl UreqMiddleware<'_> {
908    fn build_http_parts(&self) -> http_cache::Result<Parts> {
909        let method = Method::from_str(&self.method)
910            .map_err(|e| BoxError::from(format!("Invalid method: {}", e)))?;
911
912        let uri = self
913            .url
914            .parse::<http::Uri>()
915            .map_err(|e| BoxError::from(format!("Invalid URI: {}", e)))?;
916
917        let mut http_request = http::Request::builder().method(method).uri(uri);
918
919        // Add headers
920        for (name, value) in &self.headers {
921            http_request = http_request.header(name, value);
922        }
923
924        let req = http_request.body(()).map_err(|e| {
925            BoxError::from(format!("Failed to build HTTP request: {}", e))
926        })?;
927
928        Ok(req.into_parts().0)
929    }
930}
931
932#[cfg(test)]
933mod test;