Salvo框架中自定义错误处理的最佳实践
【免费下载链接】salvo 一个真正让你体感舒适的 Rust Web 后端框架 项目地址: https://gitcode.com/salvo-rs/salvo
痛点:为什么需要自定义错误处理?
在Web开发中,错误处理往往是开发者最头疼的问题之一。你是否遇到过以下场景:
- 数据库查询失败时返回给用户的错误信息过于技术化,用户无法理解
- 业务逻辑错误需要统一格式化返回,但每个Handler都要重复编写错误处理代码
- 需要根据不同的错误类型返回不同的HTTP状态码,但手动管理十分繁琐
- 错误日志记录不完整,难以排查线上问题
Salvo框架提供了一套强大而灵活的错误处理机制,让你能够以声明式的方式处理各种错误场景,提升开发效率和用户体验。
Salvo错误处理体系概览
Salvo的错误处理体系基于几个核心组件:
核心错误类型
Salvo提供了两种主要的错误类型:
StatusError- HTTP状态错误,包含详细的错误信息Error- 框架内部错误,可转换为StatusError
基础用法:快速上手
1. 返回简单的状态错误
use salvo::prelude::*;
#[handler]
async fn get_user(id: PathParam<u64>) -> Result<Json<User>, StatusError> {
let user = User::find_by_id(*id).await
.ok_or_else(|| StatusError::not_found().brief("用户不存在"))?;
Ok(Json(user))
}
#[handler]
async fn create_user(body: JsonBody<UserCreate>) -> Result<StatusCode, StatusError> {
if User::exists_by_email(&body.email).await {
return Err(StatusError::conflict()
.brief("邮箱已存在")
.detail("请使用其他邮箱地址注册"));
}
User::create(body.into_inner()).await?;
Ok(StatusCode::CREATED)
}
2. 自定义错误页面
use salvo::catcher::Catcher;
#[handler]
async fn handle_404(_req: &Request, _depot: &Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
if res.status_code == Some(StatusCode::NOT_FOUND) {
res.render(Text::Html(r#"
<!DOCTYPE html>
<html>
<head><title>页面未找到</title></head>
<body>
<h1>404 - 页面未找到</h1>
<p>抱歉,您访问的页面不存在。</p>
<a href="/">返回首页</a>
</body>
</html>
"#));
ctrl.skip_rest();
}
}
fn create_service() -> Service {
let router = Router::new()
.get(hello)
.push(Router::with_path("users").get(get_users));
Service::new(router).catcher(Catcher::default().hoop(handle_404))
}
高级实践:自定义错误类型
1. 定义业务错误枚举
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("数据库错误: {0}")]
Database(#[from] sqlx::Error),
#[error("用户不存在")]
UserNotFound,
#[error("权限不足")]
PermissionDenied,
#[error("验证失败: {0}")]
Validation(String),
#[error("第三方服务错误: {0}")]
ThirdPartyService(String),
}
impl AppError {
pub fn to_status_error(&self) -> StatusError {
match self {
AppError::Database(e) => {
tracing::error!("数据库错误: {}", e);
StatusError::internal_server_error()
.brief("服务器内部错误")
.detail("请稍后重试")
}
AppError::UserNotFound => {
StatusError::not_found()
.brief("用户不存在")
.detail("请检查用户ID是否正确")
}
AppError::PermissionDenied => {
StatusError::forbidden()
.brief("权限不足")
.detail("您没有执行此操作的权限")
}
AppError::Validation(msg) => {
StatusError::bad_request()
.brief("请求参数错误")
.detail(msg)
}
AppError::ThirdPartyService(msg) => {
tracing::error!("第三方服务错误: {}", msg);
StatusError::bad_gateway()
.brief("服务暂时不可用")
.detail("请稍后重试")
}
}
}
}
impl Scribe for AppError {
fn render(self, res: &mut Response) {
self.to_status_error().render(res);
}
}
2. 使用自定义错误类型
#[handler]
async fn update_user(
id: PathParam<u64>,
body: JsonBody<UserUpdate>,
) -> Result<Json<User>, AppError> {
let mut user = User::find_by_id(*id)
.await
.ok_or(AppError::UserNotFound)?;
// 权限检查
if !user.can_edit() {
return Err(AppError::PermissionDenied);
}
// 数据验证
if let Err(e) = body.validate() {
return Err(AppError::Validation(e.to_string()));
}
user.update(body.into_inner()).await?;
Ok(Json(user))
}
错误处理中间件模式
1. 统一的错误处理中间件
#[handler]
async fn error_handler(
&self,
req: &Request,
depot: &Depot,
res: &mut Response,
ctrl: &mut FlowCtrl,
) {
if let Some(error) = res.take_error::<AppError>() {
error.render(res);
ctrl.skip_rest();
} else if let Some(error) = res.take_error::<salvo::Error>() {
tracing::error!("未处理的框架错误: {:?}", error);
StatusError::internal_server_error()
.brief("服务器内部错误")
.render(res);
ctrl.skip_rest();
}
}
// 在路由中使用
let router = Router::new()
.hoop(error_handler)
.push(Router::with_path("api").push(
Router::with_path("users")
.get(get_users)
.post(create_user)
.push(Router::with_path("{id}")
.get(get_user)
.put(update_user)
.delete(delete_user)
)
));
2. 错误日志记录中间件
#[handler]
async fn logging_middleware(
&self,
req: &Request,
depot: &Depot,
res: &mut Response,
ctrl: &mut FlowCtrl,
) {
ctrl.call_next(req, depot, res).await;
if let Some(status_code) = res.status_code {
if status_code.is_server_error() {
if let Some(error) = res.error() {
tracing::error!(
"服务器错误: {} {} - {}",
req.method(),
req.uri(),
error
);
}
} else if status_code.is_client_error() {
if let Some(error) = res.error() {
tracing::warn!(
"客户端错误: {} {} - {}",
req.method(),
req.uri(),
error
);
}
}
}
}
OpenAPI集成的最佳实践
1. 为OpenAPI定义错误响应
use salvo::oapi::endpoint;
#[derive(Serialize, ToSchema)]
pub struct ErrorResponse {
code: u16,
message: String,
details: Option<String>,
}
#[endpoint(
responses(
(status_code = 200, description = "成功获取用户"),
(status_code = 404, description = "用户不存在", body = ErrorResponse),
(status_code = 500, description = "服务器内部错误", body = ErrorResponse)
)
)]
async fn get_user(id: PathParam<u64>) -> Result<Json<User>, StatusError> {
let user = User::find_by_id(*id).await
.ok_or_else(|| {
StatusError::not_found()
.brief("用户不存在")
.detail(format!("用户ID {} 不存在", id))
})?;
Ok(Json(user))
}
2. 统一的OpenAPI错误响应
pub fn openapi_error_responses() -> Vec<salvo::oapi::Response> {
vec![
(
StatusCode::BAD_REQUEST,
"请求参数错误",
ErrorResponse::schema(),
),
(
StatusCode::UNAUTHORIZED,
"未授权访问",
ErrorResponse::schema(),
),
(
StatusCode::FORBIDDEN,
"权限不足",
ErrorResponse::schema(),
),
(
StatusCode::NOT_FOUND,
"资源不存在",
ErrorResponse::schema(),
),
(
StatusCode::INTERNAL_SERVER_ERROR,
"服务器内部错误",
ErrorResponse::schema(),
),
]
.into_iter()
.map(|(code, desc, schema)| {
salvo::oapi::Response::new(code.as_u16())
.description(desc)
.add_content("application/json", schema)
})
.collect()
}
// 在endpoint中使用
#[endpoint(responses = openapi_error_responses())]
async fn create_user(body: JsonBody<UserCreate>) -> Result<StatusCode, StatusError> {
// 业务逻辑
}
性能优化技巧
1. 避免不必要的错误分配
// ❌ 不推荐:每次都会创建新的String
fn bad_example() -> Result<(), StatusError> {
Err(StatusError::bad_request()
.brief("错误发生".to_string())
.detail("详细错误信息".to_string()))
}
// ✅ 推荐:使用字符串字面量
fn good_example() -> Result<(), StatusError> {
Err(StatusError::bad_request()
.brief("错误发生")
.detail("详细错误信息"))
}
// ✅ 更推荐:预定义错误常量
const USER_NOT_FOUND: StatusError = StatusError::not_found()
.brief("用户不存在");
fn get_user(id: u64) -> Result<User, StatusError> {
User::find(id).ok_or(USER_NOT_FOUND)
}
2. 使用懒加载的错误信息
use std::sync::LazyLock;
static COMMON_ERRORS: LazyLock<CommonErrors> = LazyLock::new(|| CommonErrors {
user_not_found: StatusError::not_found().brief("用户不存在"),
permission_denied: StatusError::forbidden().brief("权限不足"),
// ... 其他常用错误
});
struct CommonErrors {
user_not_found: StatusError,
permission_denied: StatusError,
}
fn get_user(id: u64) -> Result<User, StatusError> {
User::find(id).ok_or(COMMON_ERRORS.user_not_found.clone())
}
测试策略
1. 错误处理单元测试
#[cfg(test)]
mod tests {
use super::*;
use salvo::test::TestClient;
use salvo::http::StatusCode;
#[tokio::test]
async fn test_user_not_found_error() {
let service = create_service();
let response = TestClient::get("http://127.0.0.1:5800/api/users/999")
.send(&service)
.await;
assert_eq!(response.status_code, Some(StatusCode::NOT_FOUND));
let error: ErrorResponse = response.json().await;
assert_eq!(error.code, 404);
assert_eq!(error.message, "用户不存在");
}
#[tokio::test]
async fn test_validation_error() {
let service = create_service();
let invalid_data = json!({
"email": "invalid-email",
"name": ""
});
let response = TestClient::post("http://127.0.0.1:5800/api/users")
.json(&invalid_data)
.send(&service)
.await;
assert_eq!(response.status_code, Some(StatusCode::BAD_REQUEST));
let error: ErrorResponse = response.json().await;
assert_eq!(error.code, 400);
assert!(error.message.contains("验证失败"));
}
}
2. 集成测试配置
fn create_test_service() -> Service {
let router = Router::new()
.hoop(Logger::new())
.hoop(error_handler)
.push(api_routes());
Service::new(router).catcher(
Catcher::default()
.hoop(handle_404)
.hoop(handle_500)
)
}
总结与最佳实践清单
通过本文的学习,你应该掌握了Salvo框架中自定义错误处理的核心技巧。以下是关键要点的总结:
🎯 核心原则
- 一致性:所有错误响应保持统一的格式和结构
- 可读性:错误信息要对用户友好,同时包含足够的调试信息
- 可维护性:错误处理逻辑要集中管理,避免重复代码
📋 最佳实践清单
| 实践领域 | 推荐做法 | 避免做法 |
|---|---|---|
| 错误定义 | 使用枚举定义业务错误 | 到处使用字符串错误 |
| 错误转换 | 实现Scribe trait统一渲染 | 每个Handler单独处理错误 |
| 日志记录 | 在中间件中统一记录错误日志 | 在业务代码中分散记录 |
| OpenAPI | 为错误响应定义Schema | 忽略API文档中的错误响应 |
| 性能 | 预定义常用错误常量 | 每次创建新的错误实例 |
🔧 工具推荐
- thiserror:用于定义丰富的错误类型
- tracing:用于结构化的错误日志记录
- Salvo TestClient:用于错误处理的集成测试
🚀 下一步行动
- 评估现有项目:检查当前项目的错误处理是否遵循这些最佳实践
- 逐步重构:选择最关键的错误处理场景开始重构
- 建立规范:为团队制定统一的错误处理规范
- 监控改进:通过日志分析错误处理改进的效果
Salvo框架的错误处理机制既强大又灵活,通过合理的使用可以显著提升应用的健壮性和可维护性。记住,好的错误处理不仅仅是技术实现,更是对用户体验的重视和对系统稳定性的保障。
【免费下载链接】salvo 一个真正让你体感舒适的 Rust Web 后端框架 项目地址: https://gitcode.com/salvo-rs/salvo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



