salvo-timeout
Salvo Request Timeout
Timeout lives in salvo_extra behind a feature flag — it is NOT on by default.
[dependencies]
salvo = { version = "0.89.3", features = ["timeout"] }
Timeout is re-exported via salvo::prelude::Timeout.
Basic usage
use std::time::Duration;
use salvo::prelude::*;
#[handler]
async fn slow() -> &'static str {
tokio::time::sleep(Duration::from_secs(10)).await;
"done"
}
#[tokio::main]
async fn main() {
let router = Router::new()
.hoop(Timeout::new(Duration::from_secs(5)))
.push(Router::with_path("slow").get(slow));
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
GOTCHA: on timeout Salvo returns 503 Service Unavailable, not 408 Request Timeout. This is intentional — some browsers retry 408 automatically. The response also includes Connection: close.
Custom error
Override the error via .error(|| StatusError):
use salvo::http::StatusError;
let timeout = Timeout::new(Duration::from_secs(5))
.error(|| StatusError::gateway_timeout().brief("Upstream too slow."));
Per-route timeouts
let router = Router::new()
.push(Router::with_path("quick")
.hoop(Timeout::new(Duration::from_secs(2)))
.get(quick_handler))
.push(Router::with_path("upload")
.hoop(Timeout::new(Duration::from_secs(120)))
.post(upload_handler))
.push(Router::with_path("report")
.hoop(Timeout::new(Duration::from_secs(300)))
.post(report_handler));
Global default + per-route override
Inner hoop runs after the outer one; a longer timeout on an inner route effectively overrides the shorter global timeout because the first handler to complete (or expire) wins the tokio::select! race.
let router = Router::new()
.hoop(Timeout::new(Duration::from_secs(30))) // default
.push(Router::with_path("health").get(health))
.push(
Router::with_path("reports/generate")
.hoop(Timeout::new(Duration::from_secs(300)))
.post(generate_report),
);
Handling timeouts in a Catcher
A Catcher can customize the 503 body:
use salvo::catcher::Catcher;
#[handler]
async fn on_timeout(res: &mut Response, ctrl: &mut FlowCtrl) {
if res.status_code == Some(StatusCode::SERVICE_UNAVAILABLE) {
res.render(Json(serde_json::json!({
"error": "timeout",
"message": "The request took too long to process",
})));
ctrl.skip_rest();
}
}
let service = Service::new(router).catcher(Catcher::default().hoop(on_timeout));
Combining with rate limiting
let router = Router::new()
.hoop(rate_limiter) // 429 on abuse
.hoop(Timeout::new(Duration::from_secs(30))) // 503 on stall
.push(Router::with_path("api/{**rest}").get(api_handler));
WebSocket / SSE note
Do NOT wrap long-lived endpoints (WebSocket upgrade, SSE) with Timeout — the middleware aborts any handler that runs longer than the duration, which will terminate these connections.
Related Skills
- salvo-concurrency-limiter: cap in-flight requests
- salvo-rate-limiter: rate limiting
- salvo-graceful-shutdown: handle in-flight requests on shutdown
More from salvo-rs/salvo-skills
salvo-csrf
Implement CSRF (Cross-Site Request Forgery) protection using cookie or session storage. Use for protecting forms and state-changing endpoints.
16salvo-auth
Implement authentication and authorization using JWT, Basic Auth, or custom schemes. Use for securing API endpoints and user management.
15salvo-cors
Configure Cross-Origin Resource Sharing (CORS) and security headers. Use for APIs accessed from browsers on different domains.
15salvo-middleware
Implement middleware for authentication, logging, CORS, and request processing. Use for cross-cutting concerns and request/response modification.
15salvo-realtime
Implement real-time features using WebSocket and Server-Sent Events (SSE). Use for chat applications, live updates, notifications, and bidirectional communication.
15salvo-caching
Implement caching strategies for improved performance. Use for reducing database load and speeding up responses.
15