ワンクリックで
salvo-caching
Implement caching strategies for improved performance. Use for reducing database load and speeding up responses.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Implement caching strategies for improved performance. Use for reducing database load and speeding up responses.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Implement authentication and authorization using JWT, Basic Auth, or custom schemes. Use for securing API endpoints and user management.
Create basic Salvo web applications with handlers, routers, and server setup. Use when starting a new Salvo project or adding basic HTTP endpoints.
Compress HTTP responses using gzip, brotli, zstd, or deflate. Use for reducing bandwidth and improving load times.
Limit concurrent requests to protect resources. Use for file uploads, expensive operations, and preventing resource exhaustion.
Configure Cross-Origin Resource Sharing (CORS) and security headers. Use for APIs accessed from browsers on different domains.
Implement CSRF (Cross-Site Request Forgery) protection using cookie or session storage. Use for protecting forms and state-changing endpoints.
| name | salvo-caching |
| description | Implement caching strategies for improved performance. Use for reducing database load and speeding up responses. |
| version | 0.94.0 |
| tags | ["performance","caching","cache-control","etag"] |
[dependencies]
salvo = { version = "0.94.0", features = ["cache", "caching-headers"] }
The cache feature activates salvo-cache with the default moka-store backend.
salvo::cache::Cache caches full responses (status, headers, body). By default it caches GET only and skips streaming responses and error bodies.
use std::time::Duration;
use salvo::cache::{Cache, MokaStore, RequestIssuer};
use salvo::prelude::*;
#[handler]
async fn expensive() -> String {
format!("computed at {}", chrono::Utc::now())
}
#[tokio::main]
async fn main() {
let cache = Cache::new(
MokaStore::builder()
.time_to_live(Duration::from_secs(60))
.max_capacity(10_000)
.build(),
RequestIssuer::default(),
);
let router = Router::new().hoop(cache).get(expensive);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
Cache::new(store, issuer) is the only constructor. Tunable:
.skipper(impl Skipper) — default is MethodSkipper::new().skip_all().skip_get(false) (GET only). Pass a closure |req, depot| bool to override.Builds cache keys from the request URI and method. Toggle parts:
let issuer = RequestIssuer::new()
.use_scheme(false)
.use_authority(false)
.use_path(true)
.use_query(true)
.use_method(true);
All five are true by default.
Implement CacheIssuer (or pass a closure) to vary cache keys by user, tenant, etc.:
use salvo::cache::CacheIssuer;
let issuer = |req: &mut Request, depot: &Depot| -> Option<String> {
let user_id = depot.get::<String>("user_id").ok()?;
Some(format!("{user_id}:{}", req.uri().path()))
};
let cache = Cache::new(store, issuer);
Returning None disables caching for that request.
salvo::caching_headers::CachingHeaders adds ETag and Last-Modified handling to downstream handlers, responding 304 when If-None-Match / If-Modified-Since match:
use salvo::caching_headers::CachingHeaders;
let router = Router::new()
.hoop(CachingHeaders::new())
.get(handler);
For ETag-only or Last-Modified-only, use ETag::new() or Modified::new() from the same module.
#[handler]
async fn cached(res: &mut Response) -> &'static str {
res.headers_mut().insert(
"cache-control",
"public, max-age=3600, stale-while-revalidate=86400".parse().unwrap(),
);
"hello"
}
Directives cheat sheet:
public, max-age=N — shared caches may store for N secondsprivate, max-age=N — browser onlyno-store — never cacheno-cache — must revalidate with originstale-while-revalidate=N — serve stale for N seconds while revalidatingStatusError has no not_modified(). Set the status directly:
#[handler]
async fn with_etag(req: &mut Request, res: &mut Response) {
let etag = compute_etag();
if req.header::<String>("if-none-match").as_deref() == Some(etag.as_str()) {
res.status_code(StatusCode::NOT_MODIFIED);
return;
}
res.headers_mut().insert("etag", etag.parse().unwrap());
res.render(Json(load_data().await));
}
For caching values inside handlers (not full responses), use moka::future::Cache directly and share via affix_state:
moka = { version = "0.12", features = ["future"] }
use moka::future::Cache as MokaCache;
use std::sync::Arc;
type UserCache = Arc<MokaCache<i64, User>>;
#[handler]
async fn get_user(req: &mut Request, depot: &mut Depot) -> Result<Json<User>, StatusError> {
let id = req.param::<i64>("id").ok_or_else(StatusError::bad_request)?;
let cache = depot.get_typed::<UserCache>().unwrap();
let pool = depot.get_typed::<PgPool>().unwrap();
if let Some(user) = cache.get(&id).await { return Ok(Json(user)); }
let user = sqlx::query_as::<_, User>("select id, name from users where id = $1")
.bind(id).fetch_optional(pool).await
.map_err(|_| StatusError::internal_server_error())?
.ok_or_else(StatusError::not_found)?;
cache.insert(id, user.clone()).await;
Ok(Json(user))
}
#[tokio::main]
async fn main() {
let cache: UserCache = Arc::new(
MokaCache::builder()
.max_capacity(1_000)
.time_to_live(std::time::Duration::from_secs(60))
.build(),
);
let router = Router::new()
.hoop(affix_state::inject(cache))
.push(Router::with_path("users/{id}").get(get_user));
// ...
}
Invalidate on write:
#[handler]
async fn update_user(req: &mut Request, depot: &mut Depot) -> StatusCode {
let id = req.param::<i64>("id").unwrap();
// update db...
depot.get_typed::<UserCache>().unwrap().invalidate(&id).await;
StatusCode::OK
}
invalidate_all() clears everything; iterate over IDs for bulk invalidation.
Cache is full-response. It needs a deterministic key — beware caching per-user data under a shared RequestIssuer; use a custom issuer keyed on user ID.ResBody::Stream) and error bodies are never cached, silently.Cache::new does not return a builder — configure the store via MokaStore::builder().MokaStore::builder() (not Cache::builder), RequestIssuer::default() or new().StatusError::not_modified() does NOT exist — set StatusCode::NOT_MODIFIED directly.Cache so the stored body is already gzipped.serve-static sets ETag/Last-Modified automatically.