use axum::{
error_handling::HandleErrorLayer,
extract::{Query, State},
response::{IntoResponse, Response},
routing::{delete, get},
BoxError, Router,
};
use http::{Request, StatusCode};
use http_cache::CACacheManager;
use http_cache_tower_server::{
CacheMetrics, CustomKeyer, QueryKeyer, ServerCacheLayer, ServerCacheOptions,
};
use serde::Deserialize;
use std::{sync::Arc, time::Duration};
use tempfile::TempDir;
use tower::ServiceBuilder;
#[derive(Clone)]
struct AppState {
metrics: Arc<CacheMetrics>,
cache_layer: Arc<ServerCacheLayer<CACacheManager, QueryKeyer>>,
}
#[tokio::main]
async fn main() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let manager = CACacheManager::new(temp_dir.path().to_path_buf(), false);
let options = ServerCacheOptions {
default_ttl: Some(Duration::from_secs(120)),
max_ttl: Some(Duration::from_secs(3600)),
cache_status_headers: true,
..Default::default()
};
let cache_layer =
ServerCacheLayer::with_keyer(manager, QueryKeyer).with_options(options);
let state = AppState {
metrics: cache_layer.metrics().clone(),
cache_layer: Arc::new(cache_layer.clone()),
};
let cached_routes = Router::new()
.route("/search", get(search))
.route("/dashboard", get(dashboard))
.route("/products/{id}", get(get_product))
.layer(
ServiceBuilder::new()
.layer(HandleErrorLayer::new(handle_cache_error))
.layer(cache_layer),
);
let admin_routes = Router::new()
.route("/metrics", get(metrics))
.route("/cache", delete(invalidate_cache));
let app = Router::new()
.merge(cached_routes)
.merge(admin_routes)
.with_state(state);
let listener =
tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Server running at http://localhost:3000");
println!();
println!("Endpoints:");
println!(" GET /search?q=... - Cached by query params");
println!(" GET /dashboard - User-specific content");
println!(" GET /products/:id - Product details");
println!(" GET /metrics - Cache statistics");
println!(" DELETE /cache?key=... - Invalidate cache entry");
axum::serve(listener, app).await.unwrap();
}
async fn handle_cache_error(err: BoxError) -> Response {
(StatusCode::INTERNAL_SERVER_ERROR, format!("Cache error: {}", err))
.into_response()
}
#[derive(Deserialize)]
struct SearchQuery {
q: String,
}
async fn search(Query(params): Query<SearchQuery>) -> Response {
tokio::time::sleep(Duration::from_millis(50)).await;
(
StatusCode::OK,
[("cache-control", "public, max-age=300")],
format!("Search results for: {}", params.q),
)
.into_response()
}
async fn dashboard() -> Response {
(
StatusCode::OK,
[("cache-control", "private, max-age=60")],
"User dashboard - private cache only",
)
.into_response()
}
async fn get_product(
axum::extract::Path(id): axum::extract::Path<u32>,
) -> Response {
tokio::time::sleep(Duration::from_millis(100)).await;
(
StatusCode::OK,
[("cache-control", "public, max-age=600")],
format!("Product {} details - cached for 10 minutes", id),
)
.into_response()
}
async fn metrics(State(state): State<AppState>) -> Response {
let metrics = &state.metrics;
let hits = metrics.hits.load(std::sync::atomic::Ordering::Relaxed);
let misses = metrics.misses.load(std::sync::atomic::Ordering::Relaxed);
let stores = metrics.stores.load(std::sync::atomic::Ordering::Relaxed);
let total = hits + misses;
let hit_rate =
if total > 0 { (hits as f64 / total as f64) * 100.0 } else { 0.0 };
let body = format!(
"Cache Metrics:\n Hits: {}\n Misses: {}\n Stores: {}\n Hit Rate: {:.1}%",
hits, misses, stores, hit_rate
);
(StatusCode::OK, [("cache-control", "no-store")], body).into_response()
}
#[derive(Deserialize)]
struct InvalidateQuery {
key: String,
}
async fn invalidate_cache(
State(state): State<AppState>,
Query(params): Query<InvalidateQuery>,
) -> Response {
match state.cache_layer.invalidate(¶ms.key).await {
Ok(()) => {
(StatusCode::OK, format!("Invalidated cache key: {}", params.key))
.into_response()
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to invalidate: {}", e),
)
.into_response(),
}
}
#[allow(dead_code)]
fn create_session_cache_layer(
manager: CACacheManager,
) -> ServerCacheLayer<
CACacheManager,
CustomKeyer<impl Fn(&Request<()>) -> String + Clone>,
> {
let keyer = CustomKeyer::new(|req: &Request<()>| {
let session = req
.headers()
.get("cookie")
.and_then(|v| v.to_str().ok())
.and_then(|cookies| {
cookies
.split(';')
.find_map(|c| c.trim().strip_prefix("session="))
})
.unwrap_or("anonymous");
format!("{} {} session:{}", req.method(), req.uri().path(), session)
});
ServerCacheLayer::with_keyer(manager, keyer)
}