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                    .entry(XCACHE.to_string())
588                    .or_insert_with(Vec::new)
589                    .push(HitOrMiss::MISS.to_string());
590                cached_response
591                    .headers
592                    .entry(XCACHELOOKUP.to_string())
593                    .or_insert_with(Vec::new)
594                    .push(HitOrMiss::MISS.to_string());
595            }
596
597            Ok(cached_response)
598        }
599    }
600}
601
602/// Middleware implementation for ureq integration
603struct UreqMiddleware<'a> {
604    method: String,
605    url: String,
606    headers: Vec<(String, String)>,
607    agent: &'a ureq::Agent,
608}
609
610fn is_cacheable_method(method: &str) -> bool {
611    matches!(method, "GET" | "HEAD")
612}
613
614/// Universal function to execute HTTP requests - replaces all method-specific duplication
615fn execute_request(
616    agent: &ureq::Agent,
617    method: &str,
618    url: &str,
619    headers: &[(String, String)],
620    body: Option<&str>,
621) -> Result<http::Response<ureq::Body>, ureq::Error> {
622    // Build http::Request directly - eliminates all method-specific switching
623    let mut http_request = http::Request::builder().method(method).uri(url);
624
625    // Add headers
626    for (name, value) in headers {
627        http_request = http_request.header(name, value);
628    }
629
630    // Build request with or without body
631    let request = match body {
632        Some(data) => http_request.body(data.as_bytes().to_vec()),
633        None => http_request.body(Vec::new()),
634    }
635    .map_err(|e| ureq::Error::BadUri(e.to_string()))?;
636
637    // Use ureq's universal run method - this replaces ALL the method-specific logic
638    agent.run(request)
639}
640
641#[cfg(feature = "json")]
642/// Universal function for JSON requests - eliminates method-specific duplication
643fn execute_json_request(
644    agent: &ureq::Agent,
645    method: &str,
646    url: &str,
647    headers: &[(String, String)],
648    data: serde_json::Value,
649) -> Result<http::Response<ureq::Body>, ureq::Error> {
650    let json_string = serde_json::to_string(&data).map_err(|e| {
651        ureq::Error::Io(std::io::Error::new(
652            std::io::ErrorKind::InvalidData,
653            format!("JSON serialization error: {}", e),
654        ))
655    })?;
656
657    // Just call the universal execute_request with JSON body
658    let mut json_headers = headers.to_vec();
659    json_headers
660        .push(("Content-Type".to_string(), "application/json".to_string()));
661
662    execute_request(agent, method, url, &json_headers, Some(&json_string))
663}
664
665fn convert_ureq_response_to_http_response(
666    mut response: http::Response<ureq::Body>,
667    url: &str,
668) -> Result<HttpResponse, HttpCacheError> {
669    let status = response.status();
670
671    // Copy headers
672    let headers = response.headers().into();
673
674    // Read body as bytes to handle binary content (images, etc.)
675    let body = response.body_mut().read_to_vec().map_err(|e| {
676        HttpCacheError::http(Box::new(std::io::Error::other(format!(
677            "Failed to read response body: {}",
678            e
679        ))))
680    })?;
681
682    // Parse the provided URL
683    let parsed_url = Url::parse(url).map_err(|e| {
684        HttpCacheError::http(Box::new(std::io::Error::other(format!(
685            "Invalid URL '{}': {}",
686            url, e
687        ))))
688    })?;
689
690    Ok(HttpResponse {
691        body,
692        headers,
693        status: status.as_u16(),
694        url: parsed_url,
695        version: http_cache::HttpVersion::Http11,
696        metadata: None,
697    })
698}
699
700/// A response wrapper that can represent both cached and fresh responses
701#[derive(Debug)]
702pub struct CachedResponse {
703    status: u16,
704    headers: HashMap<String, Vec<String>>,
705    body: Vec<u8>,
706    url: String,
707    cached: bool,
708}
709
710impl CachedResponse {
711    /// Get the response status code
712    pub fn status(&self) -> u16 {
713        self.status
714    }
715
716    /// Get the response URL
717    pub fn url(&self) -> &str {
718        &self.url
719    }
720
721    /// Get a header value
722    pub fn header(&self, name: &str) -> Option<&str> {
723        self.headers
724            .get(name)
725            .and_then(|values| values.first().map(|s| s.as_str()))
726    }
727
728    /// Get all header names
729    pub fn headers_names(&self) -> impl Iterator<Item = &String> {
730        self.headers.keys()
731    }
732
733    /// Check if this response came from cache
734    pub fn is_cached(&self) -> bool {
735        self.cached
736    }
737
738    /// Convert the response body to a string
739    pub fn into_string(self) -> Result<String, HttpCacheError> {
740        String::from_utf8(self.body).map_err(|e| {
741            HttpCacheError::http(Box::new(std::io::Error::other(format!(
742                "Invalid UTF-8 in response body: {}",
743                e
744            ))))
745        })
746    }
747
748    /// Get the response body as bytes
749    pub fn as_bytes(&self) -> &[u8] {
750        &self.body
751    }
752
753    /// Convert to bytes, consuming the response
754    pub fn into_bytes(self) -> Vec<u8> {
755        self.body
756    }
757
758    /// Parse response body as JSON
759    #[cfg(feature = "json")]
760    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
761    pub fn into_json<T: serde::de::DeserializeOwned>(
762        self,
763    ) -> Result<T, HttpCacheError> {
764        serde_json::from_slice(&self.body).map_err(|e| {
765            HttpCacheError::http(Box::new(std::io::Error::other(format!(
766                "JSON parse error: {}",
767                e
768            ))))
769        })
770    }
771}
772
773impl CachedResponse {
774    /// Create a CachedResponse from a ureq response with a known URL
775    fn from_ureq_response(
776        mut response: http::Response<ureq::Body>,
777        url: &str,
778    ) -> Self {
779        let status = response.status().as_u16();
780
781        let mut headers = HashMap::new();
782        for (name, value) in response.headers() {
783            let value_str = value.to_str().unwrap_or("");
784            headers
785                .entry(name.as_str().to_string())
786                .or_insert_with(Vec::new)
787                .push(value_str.to_string());
788        }
789
790        // Note: Cache headers will be added by the cache system based on cache_status_headers option
791        // Don't add them here unconditionally
792
793        // Read the body as bytes to handle binary content (images, etc.)
794        let body = response.body_mut().read_to_vec().unwrap_or_default();
795
796        Self { status, headers, body, url: url.to_string(), cached: false }
797    }
798}
799
800impl From<HttpResponse> for CachedResponse {
801    fn from(response: HttpResponse) -> Self {
802        // Cache headers should already be added by the cache system
803        // based on the cache_status_headers option, so don't add them here
804        Self {
805            status: response.status,
806            headers: response.headers.into(),
807            body: response.body,
808            url: response.url.to_string(),
809            cached: true,
810        }
811    }
812}
813
814#[async_trait]
815impl Middleware for UreqMiddleware<'_> {
816    fn is_method_get_head(&self) -> bool {
817        is_cacheable_method(&self.method)
818    }
819
820    fn policy(
821        &self,
822        response: &HttpResponse,
823    ) -> http_cache::Result<CachePolicy> {
824        let parts = self.build_http_parts()?;
825        Ok(CachePolicy::new(&parts, &response.parts()?))
826    }
827
828    fn policy_with_options(
829        &self,
830        response: &HttpResponse,
831        options: CacheOptions,
832    ) -> http_cache::Result<CachePolicy> {
833        let parts = self.build_http_parts()?;
834        Ok(CachePolicy::new_options(
835            &parts,
836            &response.parts()?,
837            SystemTime::now(),
838            options,
839        ))
840    }
841
842    fn update_headers(&mut self, parts: &Parts) -> http_cache::Result<()> {
843        for (name, value) in parts.headers.iter() {
844            let value_str = value.to_str().map_err(|e| {
845                BoxError::from(format!("Invalid header value: {}", e))
846            })?;
847            self.headers
848                .push((name.as_str().to_string(), value_str.to_string()));
849        }
850        Ok(())
851    }
852
853    fn force_no_cache(&mut self) -> http_cache::Result<()> {
854        self.headers
855            .push((CACHE_CONTROL.as_str().to_string(), "no-cache".to_string()));
856        Ok(())
857    }
858
859    fn parts(&self) -> http_cache::Result<Parts> {
860        self.build_http_parts()
861    }
862
863    fn url(&self) -> http_cache::Result<Url> {
864        Url::parse(&self.url).map_err(BoxError::from)
865    }
866
867    fn method(&self) -> http_cache::Result<String> {
868        Ok(self.method.clone())
869    }
870
871    async fn remote_fetch(&mut self) -> http_cache::Result<HttpResponse> {
872        let agent = self.agent.clone();
873        let method = self.method.clone();
874        let url = self.url.clone();
875        let headers = self.headers.clone();
876
877        let url_for_conversion = url.clone();
878        let response = smol::unblock(move || {
879            execute_request(&agent, &method, &url, &headers, None)
880                .map_err(|e| e.to_string())
881        })
882        .await
883        .map_err(BoxError::from)?;
884
885        // Convert the blocking response and read body on a blocking thread
886        let http_response = smol::unblock(move || {
887            convert_ureq_response_to_http_response(
888                response,
889                &url_for_conversion,
890            )
891            .map_err(|e| e.to_string())
892        })
893        .await
894        .map_err(BoxError::from)?;
895
896        Ok(http_response)
897    }
898}
899
900impl UreqMiddleware<'_> {
901    fn build_http_parts(&self) -> http_cache::Result<Parts> {
902        let method = Method::from_str(&self.method)
903            .map_err(|e| BoxError::from(format!("Invalid method: {}", e)))?;
904
905        let uri = self
906            .url
907            .parse::<http::Uri>()
908            .map_err(|e| BoxError::from(format!("Invalid URI: {}", e)))?;
909
910        let mut http_request = http::Request::builder().method(method).uri(uri);
911
912        // Add headers
913        for (name, value) in &self.headers {
914            http_request = http_request.header(name, value);
915        }
916
917        let req = http_request.body(()).map_err(|e| {
918            BoxError::from(format!("Failed to build HTTP request: {}", e))
919        })?;
920
921        Ok(req.into_parts().0)
922    }
923}
924
925#[cfg(test)]
926mod test;