Salvo框架中自定义错误处理的最佳实践

Salvo框架中自定义错误处理的最佳实践

【免费下载链接】salvo 一个真正让你体感舒适的 Rust Web 后端框架 【免费下载链接】salvo 项目地址: https://gitcode.com/salvo-rs/salvo

痛点:为什么需要自定义错误处理?

在Web开发中,错误处理往往是开发者最头疼的问题之一。你是否遇到过以下场景:

  • 数据库查询失败时返回给用户的错误信息过于技术化,用户无法理解
  • 业务逻辑错误需要统一格式化返回,但每个Handler都要重复编写错误处理代码
  • 需要根据不同的错误类型返回不同的HTTP状态码,但手动管理十分繁琐
  • 错误日志记录不完整,难以排查线上问题

Salvo框架提供了一套强大而灵活的错误处理机制,让你能够以声明式的方式处理各种错误场景,提升开发效率和用户体验。

Salvo错误处理体系概览

Salvo的错误处理体系基于几个核心组件:

mermaid

核心错误类型

Salvo提供了两种主要的错误类型:

  1. StatusError - HTTP状态错误,包含详细的错误信息
  2. 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框架中自定义错误处理的核心技巧。以下是关键要点的总结:

🎯 核心原则

  1. 一致性:所有错误响应保持统一的格式和结构
  2. 可读性:错误信息要对用户友好,同时包含足够的调试信息
  3. 可维护性:错误处理逻辑要集中管理,避免重复代码

📋 最佳实践清单

实践领域推荐做法避免做法
错误定义使用枚举定义业务错误到处使用字符串错误
错误转换实现Scribe trait统一渲染每个Handler单独处理错误
日志记录在中间件中统一记录错误日志在业务代码中分散记录
OpenAPI为错误响应定义Schema忽略API文档中的错误响应
性能预定义常用错误常量每次创建新的错误实例

🔧 工具推荐

  1. thiserror:用于定义丰富的错误类型
  2. tracing:用于结构化的错误日志记录
  3. Salvo TestClient:用于错误处理的集成测试

🚀 下一步行动

  1. 评估现有项目:检查当前项目的错误处理是否遵循这些最佳实践
  2. 逐步重构:选择最关键的错误处理场景开始重构
  3. 建立规范:为团队制定统一的错误处理规范
  4. 监控改进:通过日志分析错误处理改进的效果

Salvo框架的错误处理机制既强大又灵活,通过合理的使用可以显著提升应用的健壮性和可维护性。记住,好的错误处理不仅仅是技术实现,更是对用户体验的重视和对系统稳定性的保障。

【免费下载链接】salvo 一个真正让你体感舒适的 Rust Web 后端框架 【免费下载链接】salvo 项目地址: https://gitcode.com/salvo-rs/salvo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值