#![forbid(unsafe_code, future_incompatible)]
#![deny(
missing_docs,
missing_debug_implementations,
missing_copy_implementations,
nonstandard_style,
unused_qualifications,
unused_import_braces,
unused_extern_crates,
trivial_casts,
trivial_numeric_casts
)]
#![allow(clippy::doc_lazy_continuation)]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub use http_cache::{BadRequest, HttpCacheError};
use std::{
collections::HashMap, result::Result, str::FromStr, time::SystemTime,
};
pub use http::request::Parts;
use http::{header::CACHE_CONTROL, Method};
use http_cache::{
url_parse, BoxError, CacheManager, CacheOptions, HitOrMiss, HttpResponse,
Middleware, Url, XCACHE, XCACHELOOKUP,
};
use http_cache_semantics::CachePolicy;
pub use http_cache::{
CacheMode, HttpCache, HttpCacheOptions, ResponseCacheModeFn,
};
#[cfg(feature = "manager-cacache")]
#[cfg_attr(docsrs, doc(cfg(feature = "manager-cacache")))]
pub use http_cache::CACacheManager;
#[cfg(feature = "manager-moka")]
#[cfg_attr(docsrs, doc(cfg(feature = "manager-moka")))]
pub use http_cache::{MokaCache, MokaCacheBuilder, MokaManager};
#[cfg(feature = "rate-limiting")]
#[cfg_attr(docsrs, doc(cfg(feature = "rate-limiting")))]
pub use http_cache::rate_limiting::{
CacheAwareRateLimiter, DirectRateLimiter, DomainRateLimiter, Quota,
};
#[derive(Debug, Clone)]
pub struct CachedAgent<T: CacheManager> {
agent: ureq::Agent,
cache: HttpCache<T>,
}
#[derive(Debug)]
pub struct CachedAgentBuilder<T: CacheManager> {
agent_config: Option<ureq::config::Config>,
cache_manager: Option<T>,
cache_mode: CacheMode,
cache_options: HttpCacheOptions,
}
impl<T: CacheManager> Default for CachedAgentBuilder<T> {
fn default() -> Self {
Self {
agent_config: None,
cache_manager: None,
cache_mode: CacheMode::Default,
cache_options: HttpCacheOptions::default(),
}
}
}
impl<T: CacheManager> CachedAgentBuilder<T> {
pub fn new() -> Self {
Self::default()
}
pub fn agent_config(mut self, config: ureq::config::Config) -> Self {
self.agent_config = Some(config);
self
}
pub fn cache_manager(mut self, manager: T) -> Self {
self.cache_manager = Some(manager);
self
}
pub fn cache_mode(mut self, mode: CacheMode) -> Self {
self.cache_mode = mode;
self
}
pub fn cache_options(mut self, options: HttpCacheOptions) -> Self {
self.cache_options = options;
self
}
pub fn build(self) -> Result<CachedAgent<T>, HttpCacheError> {
let agent = if let Some(user_config) = self.agent_config {
let mut config_builder =
ureq::config::Config::builder().http_status_as_error(false);
let timeouts = user_config.timeouts();
if timeouts.global.is_some()
|| timeouts.connect.is_some()
|| timeouts.send_request.is_some()
{
if let Some(global) = timeouts.global {
config_builder =
config_builder.timeout_global(Some(global));
}
if let Some(connect) = timeouts.connect {
config_builder =
config_builder.timeout_connect(Some(connect));
}
if let Some(send_request) = timeouts.send_request {
config_builder =
config_builder.timeout_send_request(Some(send_request));
}
}
if let Some(proxy) = user_config.proxy() {
config_builder = config_builder.proxy(Some(proxy.clone()));
}
let tls_config = user_config.tls_config();
config_builder = config_builder.tls_config(tls_config.clone());
let user_agent = user_config.user_agent();
config_builder = config_builder.user_agent(user_agent.clone());
let config = config_builder.build();
ureq::Agent::new_with_config(config)
} else {
let config = ureq::config::Config::builder()
.http_status_as_error(false)
.build();
ureq::Agent::new_with_config(config)
};
let cache_manager = self.cache_manager.ok_or_else(|| {
HttpCacheError::Cache("Cache manager is required".to_string())
})?;
Ok(CachedAgent {
agent,
cache: HttpCache {
mode: self.cache_mode,
manager: cache_manager,
options: self.cache_options,
},
})
}
}
impl<T: CacheManager> CachedAgent<T> {
pub fn builder() -> CachedAgentBuilder<T> {
CachedAgentBuilder::new()
}
pub fn get(&self, url: &str) -> CachedRequestBuilder<'_, T> {
CachedRequestBuilder {
agent: self,
method: "GET".to_string(),
url: url.to_string(),
headers: Vec::new(),
}
}
pub fn post(&self, url: &str) -> CachedRequestBuilder<'_, T> {
CachedRequestBuilder {
agent: self,
method: "POST".to_string(),
url: url.to_string(),
headers: Vec::new(),
}
}
pub fn put(&self, url: &str) -> CachedRequestBuilder<'_, T> {
CachedRequestBuilder {
agent: self,
method: "PUT".to_string(),
url: url.to_string(),
headers: Vec::new(),
}
}
pub fn delete(&self, url: &str) -> CachedRequestBuilder<'_, T> {
CachedRequestBuilder {
agent: self,
method: "DELETE".to_string(),
url: url.to_string(),
headers: Vec::new(),
}
}
pub fn head(&self, url: &str) -> CachedRequestBuilder<'_, T> {
CachedRequestBuilder {
agent: self,
method: "HEAD".to_string(),
url: url.to_string(),
headers: Vec::new(),
}
}
pub fn request(
&self,
method: &str,
url: &str,
) -> CachedRequestBuilder<'_, T> {
CachedRequestBuilder {
agent: self,
method: method.to_string(),
url: url.to_string(),
headers: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct CachedRequestBuilder<'a, T: CacheManager> {
agent: &'a CachedAgent<T>,
method: String,
url: String,
headers: Vec<(String, String)>,
}
impl<'a, T: CacheManager> CachedRequestBuilder<'a, T> {
pub fn set(mut self, header: &str, value: &str) -> Self {
self.headers.push((header.to_string(), value.to_string()));
self
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
pub async fn send_json(
self,
data: serde_json::Value,
) -> Result<CachedResponse, HttpCacheError> {
let agent = self.agent.agent.clone();
let url = self.url.clone();
let method = self.method;
let headers = self.headers.clone();
let url_for_response = url.clone();
let response = smol::unblock(move || {
execute_json_request(&agent, &method, &url, &headers, data).map_err(
|e| {
HttpCacheError::http(Box::new(std::io::Error::other(
e.to_string(),
)))
},
)
})
.await?;
let cached = smol::unblock(move || {
CachedResponse::from_ureq_response(response, &url_for_response)
})
.await?;
Ok(cached)
}
pub async fn send_string(
self,
data: &str,
) -> Result<CachedResponse, HttpCacheError> {
let data = data.to_string();
let agent = self.agent.agent.clone();
let url = self.url.clone();
let method = self.method;
let headers = self.headers.clone();
let url_for_response = url.clone();
let response = smol::unblock(move || {
execute_request(&agent, &method, &url, &headers, Some(&data))
.map_err(|e| {
HttpCacheError::http(Box::new(std::io::Error::other(
e.to_string(),
)))
})
})
.await?;
let cached = smol::unblock(move || {
CachedResponse::from_ureq_response(response, &url_for_response)
})
.await?;
Ok(cached)
}
pub async fn call(self) -> Result<CachedResponse, HttpCacheError> {
let mut middleware = UreqMiddleware {
method: self.method.to_string(),
url: self.url.clone(),
headers: self.headers.clone(),
agent: &self.agent.agent,
};
if self
.agent
.cache
.can_cache_request(&middleware)
.map_err(|e| HttpCacheError::Cache(e.to_string()))?
{
let response = self
.agent
.cache
.run(middleware)
.await
.map_err(|e| HttpCacheError::Cache(e.to_string()))?;
Ok(CachedResponse::from(response))
} else {
self.agent
.cache
.run_no_cache(&mut middleware)
.await
.map_err(|e| HttpCacheError::Cache(e.to_string()))?;
let agent = self.agent.agent.clone();
let url = self.url.clone();
let method = self.method;
let headers = self.headers.clone();
let url_for_response = url.clone();
let cache_status_headers =
self.agent.cache.options.cache_status_headers;
let response = smol::unblock(move || {
execute_request(&agent, &method, &url, &headers, None).map_err(
|e| {
HttpCacheError::http(Box::new(std::io::Error::other(
e.to_string(),
)))
},
)
})
.await?;
let mut cached_response = smol::unblock(move || {
CachedResponse::from_ureq_response(response, &url_for_response)
})
.await?;
if cache_status_headers {
cached_response
.headers
.entry(XCACHE.to_string())
.or_insert_with(Vec::new)
.push(HitOrMiss::MISS.to_string());
cached_response
.headers
.entry(XCACHELOOKUP.to_string())
.or_insert_with(Vec::new)
.push(HitOrMiss::MISS.to_string());
}
Ok(cached_response)
}
}
}
struct UreqMiddleware<'a> {
method: String,
url: String,
headers: Vec<(String, String)>,
agent: &'a ureq::Agent,
}
fn is_cacheable_method(method: &str) -> bool {
matches!(method, "GET" | "HEAD")
}
fn execute_request(
agent: &ureq::Agent,
method: &str,
url: &str,
headers: &[(String, String)],
body: Option<&str>,
) -> Result<http::Response<ureq::Body>, ureq::Error> {
let mut http_request = http::Request::builder().method(method).uri(url);
for (name, value) in headers {
http_request = http_request.header(name, value);
}
let request = match body {
Some(data) => http_request.body(data.as_bytes().to_vec()),
None => http_request.body(Vec::new()),
}
.map_err(|e| ureq::Error::BadUri(e.to_string()))?;
agent.run(request)
}
#[cfg(feature = "json")]
fn execute_json_request(
agent: &ureq::Agent,
method: &str,
url: &str,
headers: &[(String, String)],
data: serde_json::Value,
) -> Result<http::Response<ureq::Body>, ureq::Error> {
let json_string = serde_json::to_string(&data).map_err(|e| {
ureq::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("JSON serialization error: {}", e),
))
})?;
let mut json_headers = headers.to_vec();
json_headers
.push(("Content-Type".to_string(), "application/json".to_string()));
execute_request(agent, method, url, &json_headers, Some(&json_string))
}
fn convert_ureq_response_to_http_response(
mut response: http::Response<ureq::Body>,
url: &str,
) -> Result<HttpResponse, HttpCacheError> {
let status = response.status();
let headers = response.headers().into();
let body = response.body_mut().read_to_vec().map_err(|e| {
HttpCacheError::http(Box::new(std::io::Error::other(format!(
"Failed to read response body: {}",
e
))))
})?;
let parsed_url = url_parse(url).map_err(|e| {
HttpCacheError::http(Box::new(std::io::Error::other(format!(
"Invalid URL '{}': {}",
url, e
))))
})?;
Ok(HttpResponse {
body,
headers,
status: status.as_u16(),
url: parsed_url,
version: http_cache::HttpVersion::Http11,
metadata: None,
})
}
#[derive(Debug)]
pub struct CachedResponse {
status: u16,
headers: HashMap<String, Vec<String>>,
body: Vec<u8>,
url: String,
cached: bool,
}
impl CachedResponse {
pub fn status(&self) -> u16 {
self.status
}
pub fn url(&self) -> &str {
&self.url
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.get(name)
.and_then(|values| values.first().map(|s| s.as_str()))
}
pub fn headers_names(&self) -> impl Iterator<Item = &String> {
self.headers.keys()
}
pub fn is_cached(&self) -> bool {
self.cached
}
pub fn into_string(self) -> Result<String, HttpCacheError> {
String::from_utf8(self.body).map_err(|e| {
HttpCacheError::http(Box::new(std::io::Error::other(format!(
"Invalid UTF-8 in response body: {}",
e
))))
})
}
pub fn as_bytes(&self) -> &[u8] {
&self.body
}
pub fn into_bytes(self) -> Vec<u8> {
self.body
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
pub fn into_json<T: serde::de::DeserializeOwned>(
self,
) -> Result<T, HttpCacheError> {
serde_json::from_slice(&self.body).map_err(|e| {
HttpCacheError::http(Box::new(std::io::Error::other(format!(
"JSON parse error: {}",
e
))))
})
}
}
impl CachedResponse {
fn from_ureq_response(
mut response: http::Response<ureq::Body>,
url: &str,
) -> Result<Self, HttpCacheError> {
let status = response.status().as_u16();
let mut headers = HashMap::new();
for (name, value) in response.headers() {
let value_str = value.to_str().unwrap_or("");
headers
.entry(name.as_str().to_string())
.or_insert_with(Vec::new)
.push(value_str.to_string());
}
let body = response.body_mut().read_to_vec().map_err(|e| {
HttpCacheError::http(Box::new(std::io::Error::other(format!(
"Failed to read response body: {}",
e
))))
})?;
Ok(Self { status, headers, body, url: url.to_string(), cached: false })
}
}
impl From<HttpResponse> for CachedResponse {
fn from(response: HttpResponse) -> Self {
Self {
status: response.status,
headers: response.headers.into(),
body: response.body,
url: response.url.to_string(),
cached: true,
}
}
}
impl Middleware for UreqMiddleware<'_> {
fn is_method_get_head(&self) -> bool {
is_cacheable_method(&self.method)
}
fn policy(
&self,
response: &HttpResponse,
) -> http_cache::Result<CachePolicy> {
let parts = self.build_http_parts()?;
Ok(CachePolicy::new(&parts, &response.parts()?))
}
fn policy_with_options(
&self,
response: &HttpResponse,
options: CacheOptions,
) -> http_cache::Result<CachePolicy> {
let parts = self.build_http_parts()?;
Ok(CachePolicy::new_options(
&parts,
&response.parts()?,
SystemTime::now(),
options,
))
}
fn update_headers(&mut self, parts: &Parts) -> http_cache::Result<()> {
for (name, value) in parts.headers.iter() {
let value_str = value.to_str().map_err(|e| {
BoxError::from(format!("Invalid header value: {}", e))
})?;
let name_str = name.as_str().to_string();
self.headers.retain(|(n, _)| n != &name_str);
self.headers.push((name_str, value_str.to_string()));
}
Ok(())
}
fn force_no_cache(&mut self) -> http_cache::Result<()> {
self.headers
.push((CACHE_CONTROL.as_str().to_string(), "no-cache".to_string()));
Ok(())
}
fn parts(&self) -> http_cache::Result<Parts> {
self.build_http_parts()
}
fn url(&self) -> http_cache::Result<Url> {
url_parse(&self.url)
}
fn method(&self) -> http_cache::Result<String> {
Ok(self.method.clone())
}
async fn remote_fetch(&mut self) -> http_cache::Result<HttpResponse> {
let agent = self.agent.clone();
let method = self.method.clone();
let url = self.url.clone();
let headers = self.headers.clone();
let url_for_conversion = url.clone();
let response = smol::unblock(move || {
execute_request(&agent, &method, &url, &headers, None)
.map_err(|e| e.to_string())
})
.await
.map_err(BoxError::from)?;
let http_response = smol::unblock(move || {
convert_ureq_response_to_http_response(
response,
&url_for_conversion,
)
.map_err(|e| e.to_string())
})
.await
.map_err(BoxError::from)?;
Ok(http_response)
}
}
impl UreqMiddleware<'_> {
fn build_http_parts(&self) -> http_cache::Result<Parts> {
let method = Method::from_str(&self.method)
.map_err(|e| BoxError::from(format!("Invalid method: {}", e)))?;
let uri = self
.url
.parse::<http::Uri>()
.map_err(|e| BoxError::from(format!("Invalid URI: {}", e)))?;
let mut http_request = http::Request::builder().method(method).uri(uri);
for (name, value) in &self.headers {
http_request = http_request.header(name, value);
}
let req = http_request.body(()).map_err(|e| {
BoxError::from(format!("Failed to build HTTP request: {}", e))
})?;
Ok(req.into_parts().0)
}
}
#[cfg(test)]
mod test;