#[cfg(test)]
mod tests {
#[cfg(feature = "streaming")]
use crate::HttpCacheStreamingLayer;
use crate::{HttpCacheBody, HttpCacheError, HttpCacheLayer};
use bytes::Bytes;
use http::{Request, Response, StatusCode};
use http_body::Body;
use http_body_util::{BodyExt, Full};
#[cfg(feature = "streaming")]
use http_cache::StreamingManager;
use http_cache::{
CACacheManager, CacheManager, CacheMode, HttpCache, HttpCacheOptions,
StreamingBody,
};
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tower::{Layer, Service, ServiceExt};
type Result<T> =
std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
const TEST_BODY: &[u8] = b"Hello, world!";
const CACHEABLE_PUBLIC: &str = "max-age=3600, public";
#[test]
fn test_errors() -> Result<()> {
let err = HttpCacheError::cache("test".to_string());
assert!(format!("{:?}", &err).contains("Cache"));
assert!(err.to_string().contains("test"));
Ok(())
}
#[derive(Clone)]
struct TestService {
status: StatusCode,
headers: Vec<(&'static str, &'static str)>,
body: &'static [u8],
}
impl TestService {
fn new(
status: StatusCode,
headers: Vec<(&'static str, &'static str)>,
body: &'static [u8],
) -> Self {
Self { status, headers, body }
}
}
impl Service<Request<Full<Bytes>>> for TestService {
type Response = Response<Full<Bytes>>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<
Box<
dyn Future<
Output = std::result::Result<
Self::Response,
Self::Error,
>,
> + Send,
>,
>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: Request<Full<Bytes>>) -> Self::Future {
let mut response = Response::builder().status(self.status);
for (name, value) in &self.headers {
response = response.header(*name, *value);
}
let response =
response.body(Full::new(Bytes::from(self.body.to_vec())));
Box::pin(async move {
response.map_err(|e| {
Box::new(e) as Box<dyn std::error::Error + Send + Sync>
})
})
}
}
#[tokio::test]
async fn default_mode() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(manager.clone());
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let request = Request::builder()
.method("GET")
.uri("http://example.com/test")
.body(Full::new(Bytes::new()))
.unwrap();
let response =
cached_service.ready().await?.call(request.clone()).await?;
assert_eq!(response.status(), StatusCode::OK);
let body_bytes = response.into_body().collect().await?.to_bytes();
assert_eq!(body_bytes, TEST_BODY);
Ok(())
}
#[tokio::test]
async fn default_mode_with_options() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let options = HttpCacheOptions::default();
let cache_layer =
HttpCacheLayer::with_options(manager.clone(), options);
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let request = Request::builder()
.method("GET")
.uri("http://example.com/test")
.body(Full::new(Bytes::new()))
.unwrap();
let response = cached_service.ready().await?.call(request).await?;
assert_eq!(response.status(), StatusCode::OK);
Ok(())
}
#[tokio::test]
async fn no_store_mode() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache = HttpCache {
mode: CacheMode::NoStore,
manager: manager.clone(),
options: HttpCacheOptions::default(),
};
let cache_layer = HttpCacheLayer::with_cache(cache);
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let request = Request::builder()
.method("GET")
.uri("http://example.com/test")
.body(Full::new(Bytes::new()))
.unwrap();
let response =
cached_service.ready().await?.call(request.clone()).await?;
assert_eq!(response.status(), StatusCode::OK);
let response = cached_service.ready().await?.call(request).await?;
assert_eq!(response.status(), StatusCode::OK);
Ok(())
}
#[tokio::test]
async fn no_cache_mode() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache = HttpCache {
mode: CacheMode::NoCache,
manager: manager.clone(),
options: HttpCacheOptions::default(),
};
let cache_layer = HttpCacheLayer::with_cache(cache);
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let request = Request::builder()
.method("GET")
.uri("http://example.com/test")
.body(Full::new(Bytes::new()))
.unwrap();
let response =
cached_service.ready().await?.call(request.clone()).await?;
assert_eq!(response.status(), StatusCode::OK);
let response = cached_service.ready().await?.call(request).await?;
assert_eq!(response.status(), StatusCode::OK);
Ok(())
}
#[tokio::test]
async fn force_cache_mode() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache = HttpCache {
mode: CacheMode::ForceCache,
manager: manager.clone(),
options: HttpCacheOptions::default(),
};
let cache_layer = HttpCacheLayer::with_cache(cache);
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", "max-age=0, public")],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let request = Request::builder()
.method("GET")
.uri("http://example.com/test")
.body(Full::new(Bytes::new()))
.unwrap();
let response =
cached_service.ready().await?.call(request.clone()).await?;
assert_eq!(response.status(), StatusCode::OK);
Ok(())
}
#[tokio::test]
async fn ignore_rules_mode() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache = HttpCache {
mode: CacheMode::IgnoreRules,
manager: manager.clone(),
options: HttpCacheOptions::default(),
};
let cache_layer = HttpCacheLayer::with_cache(cache);
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", "no-store, max-age=0, public")],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let request = Request::builder()
.method("GET")
.uri("http://example.com/test")
.body(Full::new(Bytes::new()))
.unwrap();
let response =
cached_service.ready().await?.call(request.clone()).await?;
assert_eq!(response.status(), StatusCode::OK);
Ok(())
}
#[tokio::test]
async fn post_request_bypasses_cache() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(manager.clone());
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let post_request = Request::builder()
.method("POST")
.uri("http://example.com/test")
.body(Full::new(Bytes::new()))
.unwrap();
let response = cached_service.ready().await?.call(post_request).await?;
assert_eq!(response.status(), StatusCode::OK);
Ok(())
}
#[tokio::test]
async fn layer_composition() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(manager.clone());
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let composed_service =
cache_layer.clone().layer(cache_layer.layer(test_service));
let mut service = composed_service;
let request = Request::builder()
.method("GET")
.uri("http://example.com/test")
.body(Full::new(Bytes::new()))
.unwrap();
let response = service.ready().await?.call(request).await?;
assert_eq!(response.status(), StatusCode::OK);
Ok(())
}
#[tokio::test]
async fn body_types() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(manager.clone());
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let request = Request::builder()
.method("GET")
.uri("http://example.com/test")
.body(Full::new(Bytes::new()))
.unwrap();
let response = cached_service.ready().await?.call(request).await?;
assert_eq!(response.status(), StatusCode::OK);
match response.into_body() {
HttpCacheBody::Original(body) => {
let collected = BodyExt::collect(body).await?.to_bytes();
assert!(!collected.is_empty());
}
HttpCacheBody::Buffered(data) => {
assert!(!data.is_empty());
}
}
Ok(())
}
#[tokio::test]
async fn cache_busting() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(manager.clone());
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let get_request = Request::builder()
.method("GET")
.uri("http://example.com/test")
.body(Full::new(Bytes::new()))
.unwrap();
let response = cached_service.ready().await?.call(get_request).await?;
assert_eq!(response.status(), StatusCode::OK);
let post_request = Request::builder()
.method("POST")
.uri("http://example.com/test")
.body(Full::new(Bytes::new()))
.unwrap();
let response = cached_service.ready().await?.call(post_request).await?;
assert_eq!(response.status(), StatusCode::OK);
Ok(())
}
#[tokio::test]
async fn test_conditional_requests() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(cache_manager);
#[derive(Clone)]
struct ConditionalService {
request_count: std::sync::Arc<std::sync::Mutex<usize>>,
}
impl Service<Request<Full<Bytes>>> for ConditionalService {
type Response = Response<Full<Bytes>>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<
Box<
dyn Future<
Output = std::result::Result<
Self::Response,
Self::Error,
>,
> + Send,
>,
>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<Full<Bytes>>) -> Self::Future {
let count = {
let mut count = self.request_count.lock().unwrap();
*count += 1;
*count
};
Box::pin(async move {
if req.headers().contains_key("if-none-match")
|| req.headers().contains_key("if-modified-since")
{
return Ok(Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header("cache-control", "max-age=3600, public")
.body(Full::new(Bytes::new()))?);
}
Ok(Response::builder()
.status(StatusCode::OK)
.header("cache-control", "max-age=3600, public")
.header("etag", "\"123456\"")
.header("content-type", "text/plain")
.body(Full::new(Bytes::from(format!(
"Response #{count}"
))))?)
})
}
}
let service = ConditionalService {
request_count: std::sync::Arc::new(std::sync::Mutex::new(0)),
};
let mut cached_service = cache_layer.layer(service);
let request1 = Request::builder()
.uri("https://example.com/test")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response1 = cached_service.ready().await?.call(request1).await?;
assert_eq!(response1.status(), StatusCode::OK);
let body1 = BodyExt::collect(response1.into_body()).await?.to_bytes();
assert_eq!(body1, "Response #1");
let request2 = Request::builder()
.uri("https://example.com/test")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response2 = cached_service.ready().await?.call(request2).await?;
assert_eq!(response2.status(), StatusCode::OK);
let body2 = BodyExt::collect(response2.into_body()).await?.to_bytes();
assert_eq!(body2, "Response #1");
Ok(())
}
#[tokio::test]
async fn test_response_caching_and_retrieval() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(cache_manager);
let test_service = TestService::new(
StatusCode::OK,
vec![
("cache-control", CACHEABLE_PUBLIC),
("content-type", "text/plain"),
],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let request1 = Request::builder()
.uri("https://example.com/cached")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response1 = cached_service.ready().await?.call(request1).await?;
assert_eq!(response1.status(), StatusCode::OK);
match response1.into_body() {
HttpCacheBody::Buffered(data) => {
assert_eq!(data, TEST_BODY);
}
_ => panic!("Expected Buffered body"),
}
let request2 = Request::builder()
.uri("https://example.com/cached")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response2 = cached_service.ready().await?.call(request2).await?;
assert_eq!(response2.status(), StatusCode::OK);
match response2.into_body() {
HttpCacheBody::Buffered(data) => {
assert_eq!(data, TEST_BODY);
}
_ => panic!("Expected Buffered body from cache"),
}
Ok(())
}
#[tokio::test]
async fn removes_warning() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(cache_manager);
#[derive(Clone)]
struct WarningService {
call_count: std::sync::Arc<std::sync::Mutex<usize>>,
}
impl Service<Request<Full<Bytes>>> for WarningService {
type Response = Response<Full<Bytes>>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<
Box<
dyn Future<
Output = std::result::Result<
Self::Response,
Self::Error,
>,
> + Send,
>,
>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: Request<Full<Bytes>>) -> Self::Future {
let call_count = self.call_count.clone();
Box::pin(async move {
let count = {
let mut count = call_count.lock().unwrap();
*count += 1;
*count
};
if count == 1 {
Ok(Response::builder()
.status(StatusCode::OK)
.header("cache-control", "max-age=3600, public")
.header("warning", "101 Test")
.header("content-type", "text/plain")
.body(Full::new(Bytes::from(TEST_BODY)))?)
} else {
panic!("Service called twice when response should be cached")
}
})
}
}
let service = WarningService {
call_count: std::sync::Arc::new(std::sync::Mutex::new(0)),
};
let mut cached_service = cache_layer.layer(service);
let request1 = Request::builder()
.uri("https://example.com/warning-test")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response1 = cached_service.ready().await?.call(request1).await?;
assert_eq!(response1.status(), StatusCode::OK);
assert!(response1.headers().get("warning").is_some());
let request2 = Request::builder()
.uri("https://example.com/warning-test")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response2 = cached_service.ready().await?.call(request2).await?;
assert_eq!(response2.status(), StatusCode::OK);
assert!(response2.headers().get("warning").is_none());
let body2 = BodyExt::collect(response2.into_body()).await?.to_bytes();
assert_eq!(body2, TEST_BODY);
Ok(())
}
#[tokio::test]
async fn default_mode_no_cache_response() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(cache_manager);
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", "no-cache"), ("content-type", "text/plain")],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let request1 = Request::builder()
.uri("https://example.com/no-cache")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response1 = cached_service.ready().await?.call(request1).await?;
assert_eq!(response1.status(), StatusCode::OK);
let request2 = Request::builder()
.uri("https://example.com/no-cache")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response2 = cached_service.ready().await?.call(request2).await?;
assert_eq!(response2.status(), StatusCode::OK);
Ok(())
}
#[tokio::test]
async fn revalidation_304() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(cache_manager);
#[derive(Clone)]
struct RevalidationService {
call_count: std::sync::Arc<std::sync::Mutex<usize>>,
}
impl Service<Request<Full<Bytes>>> for RevalidationService {
type Response = Response<Full<Bytes>>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<
Box<
dyn Future<
Output = std::result::Result<
Self::Response,
Self::Error,
>,
> + Send,
>,
>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<Full<Bytes>>) -> Self::Future {
let call_count = self.call_count.clone();
Box::pin(async move {
let count = {
let mut count = call_count.lock().unwrap();
*count += 1;
*count
};
if count == 1 {
Ok(Response::builder()
.status(StatusCode::OK)
.header("cache-control", "public, must-revalidate")
.header("etag", "\"123456\"")
.header("content-type", "text/plain")
.body(Full::new(Bytes::from(TEST_BODY)))?)
} else {
if req.headers().contains_key("if-none-match") {
Ok(Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header("etag", "\"123456\"")
.body(Full::new(Bytes::new()))?)
} else {
Ok(Response::builder()
.status(StatusCode::OK)
.header(
"cache-control",
"public, must-revalidate",
)
.header("etag", "\"123456\"")
.header("content-type", "text/plain")
.body(Full::new(Bytes::from(TEST_BODY)))?)
}
}
})
}
}
let service = RevalidationService {
call_count: std::sync::Arc::new(std::sync::Mutex::new(0)),
};
let mut cached_service = cache_layer.layer(service);
let request1 = Request::builder()
.uri("https://example.com/revalidate")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response1 = cached_service.ready().await?.call(request1).await?;
assert_eq!(response1.status(), StatusCode::OK);
let request2 = Request::builder()
.uri("https://example.com/revalidate")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response2 = cached_service.ready().await?.call(request2).await?;
assert_eq!(response2.status(), StatusCode::OK);
Ok(())
}
#[tokio::test]
async fn revalidation_200() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(cache_manager);
#[derive(Clone)]
struct RevalidationService {
call_count: std::sync::Arc<std::sync::Mutex<usize>>,
}
impl Service<Request<Full<Bytes>>> for RevalidationService {
type Response = Response<Full<Bytes>>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<
Box<
dyn Future<
Output = std::result::Result<
Self::Response,
Self::Error,
>,
> + Send,
>,
>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: Request<Full<Bytes>>) -> Self::Future {
let call_count = self.call_count.clone();
Box::pin(async move {
let count = {
let mut count = call_count.lock().unwrap();
*count += 1;
*count
};
if count == 1 {
Ok(Response::builder()
.status(StatusCode::OK)
.header("cache-control", "public, must-revalidate")
.header("etag", "\"123456\"")
.header("content-type", "text/plain")
.body(Full::new(Bytes::from(TEST_BODY)))?)
} else {
Ok(Response::builder()
.status(StatusCode::OK)
.header("cache-control", "public, must-revalidate")
.header("etag", "\"789012\"")
.header("content-type", "text/plain")
.body(Full::new(Bytes::from("updated")))?)
}
})
}
}
let service = RevalidationService {
call_count: std::sync::Arc::new(std::sync::Mutex::new(0)),
};
let mut cached_service = cache_layer.layer(service);
let request1 = Request::builder()
.uri("https://example.com/revalidate-200")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response1 = cached_service.ready().await?.call(request1).await?;
assert_eq!(response1.status(), StatusCode::OK);
let request2 = Request::builder()
.uri("https://example.com/revalidate-200")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response2 = cached_service.ready().await?.call(request2).await?;
assert_eq!(response2.status(), StatusCode::OK);
Ok(())
}
#[tokio::test]
async fn revalidation_500() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(cache_manager);
#[derive(Clone)]
struct RevalidationService {
call_count: std::sync::Arc<std::sync::Mutex<usize>>,
}
impl Service<Request<Full<Bytes>>> for RevalidationService {
type Response = Response<Full<Bytes>>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<
Box<
dyn Future<
Output = std::result::Result<
Self::Response,
Self::Error,
>,
> + Send,
>,
>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: Request<Full<Bytes>>) -> Self::Future {
let call_count = self.call_count.clone();
Box::pin(async move {
let count = {
let mut count = call_count.lock().unwrap();
*count += 1;
*count
};
if count == 1 {
Ok(Response::builder()
.status(StatusCode::OK)
.header("cache-control", "public, must-revalidate")
.header("etag", "\"123456\"")
.header("content-type", "text/plain")
.body(Full::new(Bytes::from(TEST_BODY)))?)
} else {
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::new(Bytes::from("Server Error")))?)
}
})
}
}
let service = RevalidationService {
call_count: std::sync::Arc::new(std::sync::Mutex::new(0)),
};
let mut cached_service = cache_layer.layer(service);
let request1 = Request::builder()
.uri("https://example.com/revalidate-500")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response1 = cached_service.ready().await?.call(request1).await?;
assert_eq!(response1.status(), StatusCode::OK);
let request2 = Request::builder()
.uri("https://example.com/revalidate-500")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response2 = cached_service.ready().await?.call(request2).await?;
assert_eq!(response2.status(), StatusCode::OK);
assert!(response2.headers().get("warning").is_some());
Ok(())
}
mod only_if_cached_mode {
use super::*;
#[tokio::test]
async fn miss() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache = HttpCache {
mode: CacheMode::OnlyIfCached,
manager: cache_manager,
options: HttpCacheOptions::default(),
};
let cache_layer = HttpCacheLayer::with_cache(cache);
let test_service = TestService::new(
StatusCode::OK,
vec![
("cache-control", "max-age=3600, public"),
("content-type", "text/plain"),
],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let request = Request::builder()
.uri("https://example.com/only-if-cached-miss")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response = cached_service.ready().await?.call(request).await?;
assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT);
Ok(())
}
#[tokio::test]
async fn hit() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_default = HttpCache {
mode: CacheMode::Default,
manager: cache_manager.clone(),
options: HttpCacheOptions::default(),
};
let cache_layer_default = HttpCacheLayer::with_cache(cache_default);
let test_service = TestService::new(
StatusCode::OK,
vec![
("cache-control", "max-age=3600, public"),
("content-type", "text/plain"),
],
TEST_BODY,
);
let mut cached_service_default =
cache_layer_default.layer(test_service.clone());
let request1 = Request::builder()
.uri("https://example.com/only-if-cached-hit")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response1 =
cached_service_default.ready().await?.call(request1).await?;
assert_eq!(response1.status(), StatusCode::OK);
let cache_only_if_cached = HttpCache {
mode: CacheMode::OnlyIfCached,
manager: cache_manager,
options: HttpCacheOptions::default(),
};
let cache_layer_only_if_cached =
HttpCacheLayer::with_cache(cache_only_if_cached);
let mut cached_service_only_if_cached =
cache_layer_only_if_cached.layer(test_service);
let request2 = Request::builder()
.uri("https://example.com/only-if-cached-hit")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response2 = cached_service_only_if_cached
.ready()
.await?
.call(request2)
.await?;
assert_eq!(response2.status(), StatusCode::OK);
Ok(())
}
}
#[cfg(feature = "streaming")]
#[tokio::test]
async fn test_streaming_cache_layer() -> Result<()> {
let cache_manager = StreamingManager::with_temp_dir(1000).await?;
let streaming_layer = HttpCacheStreamingLayer::new(cache_manager);
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let cached_service = streaming_layer.layer(test_service);
let request1 = Request::builder()
.uri("https://example.com/streaming-test")
.body(Full::new(Bytes::new()))?;
let response1 = cached_service.clone().oneshot(request1).await?;
assert_eq!(response1.status(), StatusCode::OK);
let body_bytes = response1.into_body().collect().await?.to_bytes();
assert_eq!(body_bytes, TEST_BODY);
let request2 = Request::builder()
.uri("https://example.com/streaming-test")
.body(Full::new(Bytes::new()))?;
let response2 = cached_service.clone().oneshot(request2).await?;
assert_eq!(response2.status(), StatusCode::OK);
let body_bytes2 = response2.into_body().collect().await?.to_bytes();
assert_eq!(body_bytes2, TEST_BODY);
Ok(())
}
#[tokio::test]
async fn head_request_caching() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(cache_manager.clone());
let test_service = TestService::new(
StatusCode::OK,
vec![
("cache-control", CACHEABLE_PUBLIC),
("content-type", "text/plain"),
("content-length", "13"), ],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let request = Request::builder()
.uri("https://example.com/head-test")
.method("HEAD")
.body(Full::new(Bytes::new()))?;
let response = cached_service.ready().await?.call(request).await?;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers().get("content-type").unwrap(),
"text/plain"
);
let body_bytes = response.into_body().collect().await?.to_bytes();
assert_eq!(body_bytes, TEST_BODY);
Ok(())
}
#[tokio::test]
async fn put_request_invalidates_cache() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(cache_manager.clone());
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let get_request = Request::builder()
.uri("https://example.com/invalidate-test")
.method("GET")
.body(Full::new(Bytes::new()))?;
let get_response =
cached_service.ready().await?.call(get_request).await?;
assert_eq!(get_response.status(), StatusCode::OK);
let cache_key = "GET:https://example.com/invalidate-test";
let cached_data = cache_manager.get(cache_key).await?;
assert!(cached_data.is_some());
let put_request = Request::builder()
.uri("https://example.com/invalidate-test")
.method("PUT")
.body(Full::new(Bytes::from("updated data")))?;
let put_response =
cached_service.ready().await?.call(put_request).await?;
assert_eq!(put_response.status(), StatusCode::OK);
let cached_data_after = cache_manager.get(cache_key).await?;
assert!(cached_data_after.is_none());
Ok(())
}
#[tokio::test]
async fn patch_request_invalidates_cache() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(cache_manager.clone());
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let get_request = Request::builder()
.uri("https://example.com/patch-test")
.method("GET")
.body(Full::new(Bytes::new()))?;
cached_service.ready().await?.call(get_request).await?;
let cache_key = "GET:https://example.com/patch-test";
let cached_data = cache_manager.get(cache_key).await?;
assert!(cached_data.is_some());
let patch_request = Request::builder()
.uri("https://example.com/patch-test")
.method("PATCH")
.body(Full::new(Bytes::from("patch data")))?;
let patch_response =
cached_service.ready().await?.call(patch_request).await?;
assert_eq!(patch_response.status(), StatusCode::OK);
let cached_data_after = cache_manager.get(cache_key).await?;
assert!(cached_data_after.is_none());
Ok(())
}
#[tokio::test]
async fn delete_request_invalidates_cache() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(cache_manager.clone());
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let get_request = Request::builder()
.uri("https://example.com/delete-test")
.method("GET")
.body(Full::new(Bytes::new()))?;
cached_service.ready().await?.call(get_request).await?;
let cache_key = "GET:https://example.com/delete-test";
let cached_data = cache_manager.get(cache_key).await?;
assert!(cached_data.is_some());
let delete_request = Request::builder()
.uri("https://example.com/delete-test")
.method("DELETE")
.body(Full::new(Bytes::new()))?;
let delete_response =
cached_service.ready().await?.call(delete_request).await?;
assert_eq!(delete_response.status(), StatusCode::OK);
let cached_data_after = cache_manager.get(cache_key).await?;
assert!(cached_data_after.is_none());
Ok(())
}
#[tokio::test]
async fn options_request_not_cached() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(cache_manager.clone());
let test_service_call_count =
std::sync::Arc::new(std::sync::Mutex::new(0));
let count_clone = test_service_call_count.clone();
#[derive(Clone)]
struct CountingService {
call_count: std::sync::Arc<std::sync::Mutex<usize>>,
}
impl Service<Request<Full<Bytes>>> for CountingService {
type Response = Response<Full<Bytes>>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<
Box<
dyn Future<
Output = std::result::Result<
Self::Response,
Self::Error,
>,
> + Send,
>,
>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: Request<Full<Bytes>>) -> Self::Future {
let call_count = self.call_count.clone();
Box::pin(async move {
{
let mut count = call_count.lock().unwrap();
*count += 1;
}
Ok(Response::builder()
.status(StatusCode::OK)
.header("allow", "GET, POST, PUT, DELETE")
.header("cache-control", CACHEABLE_PUBLIC) .body(Full::new(Bytes::new()))?)
})
}
}
let counting_service = CountingService { call_count: count_clone };
let mut cached_service = cache_layer.layer(counting_service);
let request1 = Request::builder()
.uri("https://example.com/options-test")
.method("OPTIONS")
.body(Full::new(Bytes::new()))?;
let response1 = cached_service.ready().await?.call(request1).await?;
assert_eq!(response1.status(), StatusCode::OK);
let cache_key = "OPTIONS:https://example.com/options-test";
let cached_data = cache_manager.get(cache_key).await?;
assert!(cached_data.is_none());
let request2 = Request::builder()
.uri("https://example.com/options-test")
.method("OPTIONS")
.body(Full::new(Bytes::new()))?;
let response2 = cached_service.ready().await?.call(request2).await?;
assert_eq!(response2.status(), StatusCode::OK);
let final_count = *test_service_call_count.lock().unwrap();
assert_eq!(final_count, 2);
Ok(())
}
#[test]
fn test_streaming_body() -> Result<()> {
let buffered_body: StreamingBody<Full<Bytes>> =
StreamingBody::buffered(Bytes::from("test data"));
assert!(!buffered_body.is_end_stream());
let size_hint = buffered_body.size_hint();
assert_eq!(size_hint.exact(), Some(9));
Ok(())
}
#[tokio::test]
async fn custom_cache_key() -> Result<()> {
use std::sync::Arc;
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let options = HttpCacheOptions {
cache_key: Some(Arc::new(|req: &http::request::Parts| {
format!("{}:{}:{:?}:test", req.method, req.uri, req.version)
})),
..Default::default()
};
let cache = HttpCache {
mode: CacheMode::Default,
manager: cache_manager.clone(),
options,
};
let cache_layer = HttpCacheLayer::with_cache(cache);
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let request = Request::builder()
.uri("https://example.com/custom-key-test")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response =
cached_service.ready().await?.call(request.clone()).await?;
assert_eq!(response.status(), StatusCode::OK);
let custom_key = format!(
"{}:{}:{:?}:test",
"GET",
"https://example.com/custom-key-test",
http::Version::HTTP_11
);
let cached_data = cache_manager.get(&custom_key).await?;
assert!(cached_data.is_some());
Ok(())
}
#[tokio::test]
async fn custom_cache_mode_fn() -> Result<()> {
use std::sync::Arc;
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let options = HttpCacheOptions {
cache_mode_fn: Some(Arc::new(|req: &http::request::Parts| {
if req.uri.path().ends_with(".css") {
CacheMode::Default
} else {
CacheMode::NoStore
}
})),
..Default::default()
};
let cache = HttpCache {
mode: CacheMode::NoStore, manager: cache_manager.clone(),
options,
};
let cache_layer = HttpCacheLayer::with_cache(cache);
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service.clone());
let css_request = Request::builder()
.uri("https://example.com/styles.css")
.method("GET")
.body(Full::new(Bytes::new()))?;
cached_service.ready().await?.call(css_request).await?;
let css_cache_key = "GET:https://example.com/styles.css";
let cached_css = cache_manager.get(css_cache_key).await?;
assert!(cached_css.is_some());
let mut cached_service2 = cache_layer.layer(test_service);
let html_request = Request::builder()
.uri("https://example.com/index.html")
.method("GET")
.body(Full::new(Bytes::new()))?;
cached_service2.ready().await?.call(html_request).await?;
let html_cache_key = "GET:https://example.com/index.html";
let cached_html = cache_manager.get(html_cache_key).await?;
assert!(cached_html.is_none());
Ok(())
}
#[tokio::test]
async fn custom_response_cache_mode_fn() -> Result<()> {
use std::sync::Arc;
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let options = HttpCacheOptions {
response_cache_mode_fn: Some(Arc::new(
|_request_parts, response| {
match response.status {
200..=299 => Some(CacheMode::ForceCache),
429 => Some(CacheMode::NoStore),
_ => None, }
},
)),
..Default::default()
};
let cache = HttpCache {
mode: CacheMode::Default,
manager: cache_manager.clone(),
options,
};
let cache_layer = HttpCacheLayer::with_cache(cache);
let success_service = TestService::new(
StatusCode::OK,
vec![
("cache-control", "no-cache, no-store, must-revalidate"),
("pragma", "no-cache"),
],
TEST_BODY,
);
let mut cached_success_service =
cache_layer.clone().layer(success_service);
let rate_limit_service = TestService::new(
StatusCode::TOO_MANY_REQUESTS,
vec![
("cache-control", "public, max-age=300"),
("retry-after", "60"),
],
b"Rate limit exceeded",
);
let mut cached_rate_limit_service =
cache_layer.layer(rate_limit_service);
let success_request = Request::builder()
.uri("https://example.com/api/data")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response =
cached_success_service.ready().await?.call(success_request).await?;
assert_eq!(response.status(), StatusCode::OK);
let success_cache_key = "GET:https://example.com/api/data";
let cached_data = cache_manager.get(success_cache_key).await?;
assert!(cached_data.is_some());
let rate_limit_request = Request::builder()
.uri("https://example.com/api/rate-limited")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response = cached_rate_limit_service
.ready()
.await?
.call(rate_limit_request)
.await?;
assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
let rate_limit_cache_key = "GET:https://example.com/api/rate-limited";
let cached_data = cache_manager.get(rate_limit_cache_key).await?;
assert!(cached_data.is_none());
Ok(())
}
#[tokio::test]
async fn custom_cache_bust() -> Result<()> {
use std::sync::Arc;
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let options = HttpCacheOptions {
cache_bust: Some(Arc::new(|req: &http::request::Parts, _, _| {
if req.uri.path().ends_with("/bust-cache") {
vec![format!(
"{}:{}://{}:{}/",
"GET",
req.uri.scheme_str().unwrap_or("https"),
req.uri.host().unwrap_or("example.com"),
req.uri.port_u16().unwrap_or(443)
)]
} else {
Vec::new()
}
})),
..Default::default()
};
let cache = HttpCache {
mode: CacheMode::Default,
manager: cache_manager.clone(),
options,
};
let cache_layer = HttpCacheLayer::with_cache(cache);
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let cache_request = Request::builder()
.uri("https://example.com:443/")
.method("GET")
.body(Full::new(Bytes::new()))?;
cached_service.ready().await?.call(cache_request).await?;
let cache_key = "GET:https://example.com:443/";
let cached_data = cache_manager.get(cache_key).await?;
assert!(cached_data.is_some());
let bust_request = Request::builder()
.uri("https://example.com:443/bust-cache")
.method("GET")
.body(Full::new(Bytes::new()))?;
cached_service.ready().await?.call(bust_request).await?;
let cached_data_after = cache_manager.get(cache_key).await?;
assert!(cached_data_after.is_none());
Ok(())
}
#[cfg(feature = "streaming")]
#[tokio::test]
async fn test_streaming_cache_large_response() -> Result<()> {
use http_cache::StreamingManager;
let cache_manager = StreamingManager::with_temp_dir(1000).await?;
let streaming_layer = HttpCacheStreamingLayer::new(cache_manager);
const LARGE_DATA: &[u8] = &[b'x'; 1024 * 1024];
let large_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
LARGE_DATA,
);
let cached_service = streaming_layer.layer(large_service);
let request1 = Request::builder()
.uri("https://example.com/large-streaming-test")
.body(Full::new(Bytes::new()))?;
let response1 = cached_service.clone().oneshot(request1).await?;
assert_eq!(response1.status(), StatusCode::OK);
let body_bytes = response1.into_body().collect().await?.to_bytes();
assert_eq!(body_bytes.len(), 1024 * 1024);
assert_eq!(body_bytes, LARGE_DATA);
let request2 = Request::builder()
.uri("https://example.com/large-streaming-test")
.body(Full::new(Bytes::new()))?;
let response2 = cached_service.clone().oneshot(request2).await?;
assert_eq!(response2.status(), StatusCode::OK);
let body_bytes2 = response2.into_body().collect().await?.to_bytes();
assert_eq!(body_bytes2.len(), 1024 * 1024);
assert_eq!(body_bytes2, LARGE_DATA);
Ok(())
}
#[cfg(feature = "streaming")]
#[tokio::test]
async fn test_streaming_cache_empty_response() -> Result<()> {
use http_cache::StreamingManager;
let cache_manager = StreamingManager::with_temp_dir(1000).await?;
let streaming_layer = HttpCacheStreamingLayer::new(cache_manager);
let empty_service = TestService::new(
StatusCode::NO_CONTENT,
vec![("cache-control", CACHEABLE_PUBLIC)],
b"", );
let cached_service = streaming_layer.layer(empty_service);
let request1 = Request::builder()
.uri("https://example.com/empty-streaming-test")
.body(Full::new(Bytes::new()))?;
let response1 = cached_service.clone().oneshot(request1).await?;
assert_eq!(response1.status(), StatusCode::NO_CONTENT);
let body_bytes = response1.into_body().collect().await?.to_bytes();
assert!(body_bytes.is_empty());
let request2 = Request::builder()
.uri("https://example.com/empty-streaming-test")
.body(Full::new(Bytes::new()))
.unwrap();
let response2 = cached_service.clone().oneshot(request2).await?;
assert_eq!(response2.status(), StatusCode::NO_CONTENT);
let body_bytes2 = response2.into_body().collect().await?.to_bytes();
assert!(body_bytes2.is_empty());
Ok(())
}
#[cfg(feature = "streaming")]
#[tokio::test]
async fn test_streaming_cache_no_cache_mode() -> Result<()> {
use http_cache::StreamingManager;
let cache_manager = StreamingManager::with_temp_dir(1000).await?;
let cache = http_cache::HttpStreamingCache {
mode: CacheMode::NoStore,
manager: cache_manager,
options: HttpCacheOptions::default(),
};
let streaming_layer = HttpCacheStreamingLayer::with_cache(cache);
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let cached_service = streaming_layer.layer(test_service);
let request = Request::builder()
.uri("https://example.com/no-cache-streaming-test")
.body(Full::new(Bytes::new()))?;
let response = cached_service.clone().oneshot(request).await?;
assert_eq!(response.status(), StatusCode::OK);
let body_bytes = response.into_body().collect().await?.to_bytes();
assert_eq!(body_bytes, TEST_BODY);
Ok(())
}
#[tokio::test]
async fn head_request_cached_like_get() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache_layer = HttpCacheLayer::new(cache_manager.clone());
#[derive(Clone)]
struct GetHeadService {
call_count: std::sync::Arc<std::sync::Mutex<usize>>,
}
impl Service<Request<Full<Bytes>>> for GetHeadService {
type Response = Response<Full<Bytes>>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<
Box<
dyn Future<
Output = std::result::Result<
Self::Response,
Self::Error,
>,
> + Send,
>,
>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<Full<Bytes>>) -> Self::Future {
let call_count = self.call_count.clone();
Box::pin(async move {
{
let mut count = call_count.lock().unwrap();
*count += 1;
}
let mut response = Response::builder()
.status(StatusCode::OK)
.header("cache-control", CACHEABLE_PUBLIC)
.header("content-type", "text/plain")
.header("etag", "\"12345\"");
let body = if req.method() == "HEAD" {
response = response.header("content-length", "13");
Full::new(Bytes::new())
} else {
Full::new(Bytes::from(TEST_BODY))
};
Ok(response.body(body)?)
})
}
}
let service = GetHeadService {
call_count: std::sync::Arc::new(std::sync::Mutex::new(0)),
};
let call_count = service.call_count.clone();
let mut cached_service = cache_layer.layer(service);
let get_request = Request::builder()
.uri("https://example.com/get-head-test")
.method("GET")
.body(Full::new(Bytes::new()))?;
let get_response =
cached_service.ready().await?.call(get_request).await?;
assert_eq!(get_response.status(), StatusCode::OK);
assert_eq!(get_response.headers().get("etag").unwrap(), "\"12345\"");
let get_body = get_response.into_body().collect().await?.to_bytes();
assert_eq!(get_body, TEST_BODY);
let head_request = Request::builder()
.uri("https://example.com/get-head-test")
.method("HEAD")
.body(Full::new(Bytes::new()))?;
let head_response =
cached_service.ready().await?.call(head_request).await?;
assert_eq!(head_response.status(), StatusCode::OK);
assert_eq!(head_response.headers().get("etag").unwrap(), "\"12345\"");
let head_body = head_response.into_body().collect().await?.to_bytes();
assert!(head_body.is_empty());
let get_cache_key = "GET:https://example.com/get-head-test";
let get_cached_data = cache_manager.get(get_cache_key).await?;
assert!(get_cached_data.is_some());
let head_cache_key = "HEAD:https://example.com/get-head-test";
let head_cached_data = cache_manager.get(head_cache_key).await?;
assert!(head_cached_data.is_some());
let final_count = *call_count.lock().unwrap();
assert_eq!(final_count, 2);
Ok(())
}
#[tokio::test]
async fn reload_mode() -> Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache_manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let cache = HttpCache {
mode: CacheMode::Reload,
manager: cache_manager.clone(),
options: HttpCacheOptions {
cache_options: Some(http_cache::CacheOptions {
shared: false,
..Default::default()
}),
..Default::default()
},
};
let cache_layer = HttpCacheLayer::with_cache(cache);
let test_service_call_count =
std::sync::Arc::new(std::sync::Mutex::new(0));
let count_clone = test_service_call_count.clone();
#[derive(Clone)]
struct ReloadTestService {
call_count: std::sync::Arc<std::sync::Mutex<usize>>,
}
impl Service<Request<Full<Bytes>>> for ReloadTestService {
type Response = Response<Full<Bytes>>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<
Box<
dyn Future<
Output = std::result::Result<
Self::Response,
Self::Error,
>,
> + Send,
>,
>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: Request<Full<Bytes>>) -> Self::Future {
let call_count = self.call_count.clone();
Box::pin(async move {
{
let mut count = call_count.lock().unwrap();
*count += 1;
}
Ok(Response::builder()
.status(StatusCode::OK)
.header("cache-control", CACHEABLE_PUBLIC)
.body(Full::new(Bytes::from(TEST_BODY)))?)
})
}
}
let reload_service = ReloadTestService { call_count: count_clone };
let mut cached_service = cache_layer.layer(reload_service);
let request1 = Request::builder()
.uri("https://example.com/reload-test")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response1 = cached_service.ready().await?.call(request1).await?;
assert_eq!(response1.status(), StatusCode::OK);
let cache_key = "GET:https://example.com/reload-test";
let cached_data = cache_manager.get(cache_key).await?;
assert!(cached_data.is_some());
let request2 = Request::builder()
.uri("https://example.com/reload-test")
.method("GET")
.body(Full::new(Bytes::new()))?;
let response2 = cached_service.ready().await?.call(request2).await?;
assert_eq!(response2.status(), StatusCode::OK);
let final_count = *test_service_call_count.lock().unwrap();
assert_eq!(final_count, 2);
Ok(())
}
#[tokio::test]
async fn test_streaming_request_graceful_handling() -> Result<()> {
let temp_dir = tempfile::TempDir::new().unwrap();
let cache_manager = CACacheManager::new(temp_dir.path().into(), true);
let cache_layer = HttpCacheLayer::new(cache_manager);
#[derive(Clone)]
struct StreamingService;
impl Service<Request<Full<Bytes>>> for StreamingService {
type Response = Response<Full<Bytes>>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<
Box<
dyn Future<
Output = std::result::Result<
Self::Response,
Self::Error,
>,
> + Send,
>,
>;
fn poll_ready(
&mut self,
_: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<Full<Bytes>>) -> Self::Future {
let (parts, body) = req.into_parts();
Box::pin(async move {
let body_bytes = BodyExt::collect(body)
.await
.map_err(|e| {
Box::new(e)
as Box<dyn std::error::Error + Send + Sync>
})?
.to_bytes();
let body_size = body_bytes.len();
Response::builder()
.status(StatusCode::OK)
.header("cache-control", "max-age=3600, public")
.header("content-type", "application/json")
.header("x-body-size", body_size.to_string())
.body(Full::new(Bytes::from(format!(
"{{\"processed\": true, \"body_size\": {}, \"uri\": \"{}\"}}",
body_size,
parts.uri
))))
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
})
}
}
let mut cached_service = cache_layer.layer(StreamingService);
let large_body_data = "streaming data ".repeat(10000); let request = Request::builder()
.uri("https://example.com/streaming-upload")
.method("POST")
.header("content-type", "application/octet-stream")
.body(Full::new(Bytes::from(large_body_data.clone())))
.map_err(|e| {
Box::new(e) as Box<dyn std::error::Error + Send + Sync>
})?;
let response = cached_service.ready().await?.call(request).await;
match response {
Ok(response) => {
assert_eq!(response.status(), StatusCode::OK);
assert!(response.headers().contains_key("x-body-size"));
}
Err(e) => {
let error_msg = e.to_string();
assert!(
!error_msg.to_lowercase().contains("clone"),
"Expected graceful handling but got cloning-related error: {}",
error_msg
);
return Err(
Box::new(e) as Box<dyn std::error::Error + Send + Sync>
);
}
}
Ok(())
}
#[tokio::test]
async fn test_metadata_retrieval_through_extensions() -> Result<()> {
use http_cache::HttpCacheMetadata;
use std::sync::Arc;
let cache_dir = tempfile::tempdir()?;
let manager =
CACacheManager::new(cache_dir.path().to_path_buf(), false);
let options = HttpCacheOptions {
metadata_provider: Some(Arc::new(
|_request_parts, _response_parts| {
Some(b"test-metadata-value".to_vec())
},
)),
..Default::default()
};
let cache_layer =
HttpCacheLayer::with_options(manager.clone(), options);
let test_service = TestService::new(
StatusCode::OK,
vec![("cache-control", CACHEABLE_PUBLIC)],
TEST_BODY,
);
let mut cached_service = cache_layer.layer(test_service);
let request = Request::builder()
.method("GET")
.uri("http://example.com/metadata-test")
.body(Full::new(Bytes::new()))
.map_err(|e| {
Box::new(e) as Box<dyn std::error::Error + Send + Sync>
})?;
let response1 = cached_service.ready().await?.call(request).await?;
let metadata1 = response1.extensions().get::<HttpCacheMetadata>();
assert!(
metadata1.is_some(),
"Metadata should be present even on cache miss (generated by provider)"
);
assert_eq!(
metadata1.unwrap().as_slice(),
b"test-metadata-value",
"Metadata on miss should match provider output"
);
let request = Request::builder()
.method("GET")
.uri("http://example.com/metadata-test")
.body(Full::new(Bytes::new()))
.map_err(|e| {
Box::new(e) as Box<dyn std::error::Error + Send + Sync>
})?;
let response2 = cached_service.ready().await?.call(request).await?;
let metadata = response2.extensions().get::<HttpCacheMetadata>();
assert!(
metadata.is_some(),
"Metadata should be present in response extensions"
);
let metadata_value = metadata.unwrap();
assert_eq!(
metadata_value.as_slice(),
b"test-metadata-value",
"Metadata value should match what was stored"
);
Ok(())
}
}