Rust Web 全栈开发(七):错误处理

Rust Web 全栈开发(七):错误处理

参考视频:https://www.bilibili.com/video/BV1RP4y1G7KF

继续之前的 Actix 项目。

统一的错误处理

我们希望在 Web Service 中实现统一了错误处理。

在这里插入图片描述

将程序运行中的各种错误,归类为自定义错误类型,再转化为 HTTP Response 发送给客户端。

Actix 框架中的错误处理

Actix-Web 定义了一个通用的错误类型结构体 actix-web::error::Error,它实现了 std::error::Error 这个 trait。

任何实现了标准库 Error trait 的类型,都可以通过 ? 运算符,转换为 Actix 的 Error 类型。Actix 的 Error 类型会自动转换为 HTTP Response,返回给客户端。

ResponseError trait:任何实现该 trait 的错误均可转化为 HTTP Response 消息。

Actix-Web 对于常见的错误有内置的实现,例如:

  • Rust 标准 I/O 错误
  • Serde 错误
  • Web 错误,例如:ProtocolError、Utf8Error、ParseError 等等

对于其他没有内置实现的错误类型,需要自定义实现错误到 HTTP Response 的转换。

创建自定义错误处理器

  1. 创建一个自定义错误类型
  2. 实现 From trait,用于把其它错误类型转化为该类型
  3. 为自定义错误类型实现 ResponseError trait
  4. 在 handler 里返回自定义错误类型
  5. Actix 会把错误转化为 HTTP 响应

在 webservice/src 目录下新建一个 errors.rs,添加以下代码:

use actix_web::{error, http::StatusCode, HttpResponse, Result};
use serde::Serialize;
use sqlx::error::Error as SQLxError;
use std::fmt;

#[derive(Debug, Serialize)]
pub enum MyError {
    DBError(String),
    ActixError(String),
    NotFound(String),
}

#[derive(Debug, Serialize)]
pub struct MyErrorResponse {
    error_message: String,
}

impl MyError {
    fn error_response(&self) -> String {
        match self {
            MyError::DBError(msg) => {
                println!("Database error occurred: {:?}", msg);
                "Database error".into()
            }
            MyError::ActixError(msg) => {
                println!("Server error occurred: {:?}", msg);
                "Internal server error".into()
            }
            MyError::NotFound(msg) => {
                println!("Not found error occurred: {:?}", msg);
                msg.into()
            }
        }
    }
}

impl error::ResponseError for MyError {
    fn status_code(&self) -> StatusCode {
        match self {
            MyError::DBError(_msg) | MyError::ActixError(_msg) => StatusCode::INTERNAL_SERVER_ERROR,
            MyError::NotFound(_msg) => StatusCode::NOT_FOUND,
        }
    }

    fn error_response(&self) -> HttpResponse {
        HttpResponse::build(self.status_code()).json(MyErrorResponse {
            error_message: self.error_response(),
        })
    }
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        write!(f, "{}", self)
    }
}

impl From<actix_web::error::Error> for MyError {
    fn from(err: actix_web::error::Error) -> Self {
        MyError::ActixError(err.to_string())
    }
}

impl From<SQLxError> for MyError {
    fn from(err: SQLxError) -> Self {
        MyError::DBError(err.to_string())
    }
}

ResponseError trait 要求实现类型实现 Debug 和 Display trait。其中,Debug trait 有默认实现,Display trait 需要自己实现。

引入 errors 模块

修改 teacher_service.rs,引入 errors 模块:

#[path = "../errors.rs"]
mod errors;

修改 db_access

修改 db_access.rs,每个数据库操作的函数的返回值改为 Result:

use super::models::*;
use sqlx::MySqlPool;
use super::errors::MyError;

pub async fn get_courses_for_teacher_db(pool: &MySqlPool, teacher_id: i32) -> Result<Vec<Course>, MyError> {
    let rows: Vec<Course> = sqlx::query_as(
        "SELECT id, teacher_id, name, time
            FROM course
            WHERE teacher_id = ?"
    )
        .bind(teacher_id)
        .fetch_all(pool) // 获取所有记录
        .await?;

    let courses: Vec<Course> = rows.iter()
        .map(|r| Course {
            id: Some(r.id.expect("Unknown")),
            teacher_id: r.teacher_id,
            name: r.name.clone(),
            time: Some(chrono::NaiveDateTime::from(r.time.unwrap())),
        })
        .collect();

    match courses.len() {
        0 => Err(MyError::NotFound("Course not found for teacher".into())),
        _ => Ok(courses),
    }
}

pub async fn get_course_details_db(pool: &MySqlPool, teacher_id: i32, course_id: i32) -> Result<Course, MyError> {
    let row: Result<Course, sqlx::Error> = sqlx::query_as(
        "SELECT id, teacher_id, name, time
            FROM course
            WHERE teacher_id = ? and id = ?"
    )
        .bind(teacher_id)
        .bind(course_id)
        .fetch_one(pool) // 获取单条记录
        .await;

    if let Ok(row) = row {
        Ok(Course {
            id: Some(row.id.expect("Unknown")),
            teacher_id: row.teacher_id,
            name: row.name.clone(),
            time: Some(chrono::NaiveDateTime::from(row.time.unwrap())),
        })
    } else {
        Err(MyError::NotFound("Course didn't founded".into()))
    }
}

pub async fn post_new_course_db(pool: &MySqlPool, new_course: Course) -> Result<(), MyError> {
    let _insert_query = sqlx::query!(
        "INSERT INTO course (id, teacher_id, name, time)
            VALUES (?, ?, ?, ?)",
        new_course.id.unwrap(),
        new_course.teacher_id,
        new_course.name,
        new_course.time
    )
        .execute(pool)
        .await?;

    Ok(())
}

修改 handler

修改 handlers.rs,同样将函数的返回值改为 Result,对应的测试函数也要修改。

use super::state::AppState;
use actix_web::{web, HttpResponse};
use super::models::Course;
use super::db_access::*;
use super::errors::MyError;

pub async fn health_check_handler(app_state: web::Data<AppState>) -> HttpResponse {
    println!("incoming for health check");

    let health_check_response = &app_state.health_check_response;
    let mut visit_count = app_state.visit_count.lock().unwrap();
    let response = format!("{} {} times", health_check_response, visit_count);
    *visit_count += 1;

    HttpResponse::Ok().json(&response)
}

pub async fn new_course(
    new_course: web::Json<Course>,
    app_state: web::Data<AppState>,
) -> Result<HttpResponse, MyError> {
    post_new_course_db(&app_state.db, new_course.into())
        .await
        .map(|course| HttpResponse::Ok().json(course))
}

pub async fn get_courses_for_teacher(
    app_state: web::Data<AppState>,
    params: web::Path<i32>,
) -> Result<HttpResponse, MyError> {
    let teacher_id = params.into_inner();
    get_courses_for_teacher_db(&app_state.db, teacher_id)
        .await
        .map(|courses| HttpResponse::Ok().json(courses))
}

pub async fn get_course_detail(
    app_state: web::Data<AppState>,
    params: web::Path<(i32, i32)>,
) -> Result<HttpResponse, MyError> {
    let (teacher_id, course_id) = params.into_inner();
    get_course_details_db(&app_state.db, teacher_id, course_id)
        .await
        .map(|course| HttpResponse::Ok().json(course))
}

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::http::StatusCode;
    use std::sync::Mutex;
    use dotenv::dotenv;
    use sqlx::mysql::MySqlPoolOptions;
    use std::env;
    use chrono::{NaiveDate, NaiveDateTime, NaiveTime};

    #[actix_rt::test]
    async fn post_course_test() {
        // 检测并读取 .env 文件中的内容,若不存在也会跳过异常
        dotenv().ok();

        let db_url = env::var("DATABASE_URL")
            .expect("DATABASE_URL 没有在 .env 文件里设置");

        // 创建数据库连接池
        let db_pool = MySqlPoolOptions::new()
            .connect(&db_url)
            .await
            .unwrap();

        let course = web::Json(Course {
            teacher_id: 1,
            name: "Test course".into(),
            id: Some(3),
            time: Some(NaiveDateTime::new(
                NaiveDate::from_ymd_opt(2025, 7, 12).expect("Unknown date"),
                NaiveTime::from_hms_opt(10, 15, 0).expect("Unknown time"),
            )),
        });

        let app_state: web::Data<AppState> = web::Data::new(AppState {
            health_check_response: "".to_string(),
            visit_count: Mutex::new(0),
            db: db_pool,
        });

        // 模拟添加课程的请求
        let response = new_course(course, app_state).await.unwrap();

        assert_eq!(response.status(), StatusCode::OK);
    }

    #[actix_rt::test]
    async fn get_all_courses_success() {
        // 检测并读取 .env 文件中的内容,若不存在也会跳过异常
        dotenv().ok();

        let db_url = env::var("DATABASE_URL")
            .expect("DATABASE_URL 没有在 .env 文件里设置");

        // 创建数据库连接池
        let db_pool = MySqlPoolOptions::new()
            .connect(&db_url)
            .await
            .unwrap();

        let app_state: web::Data<AppState> = web::Data::new(AppState {
            health_check_response: "".to_string(),
            visit_count: Mutex::new(0),
            db: db_pool,
        });

        let teacher_id: web::Path<i32> = web::Path::from(1);
        let response = get_courses_for_teacher(app_state, teacher_id).await.unwrap();

        assert_eq!(response.status(), StatusCode::OK);
    }

    #[actix_rt::test]
    async fn get_one_course_success() {
        // 检测并读取 .env 文件中的内容,若不存在也会跳过异常
        dotenv().ok();

        let db_url = env::var("DATABASE_URL")
            .expect("DATABASE_URL 没有在 .env 文件里设置");

        // 创建数据库连接池
        let db_pool = MySqlPoolOptions::new()
            .connect(&db_url)
            .await
            .unwrap();

        let app_state: web::Data<AppState> = web::Data::new(AppState {
            health_check_response: "".to_string(),
            visit_count: Mutex::new(0),
            db: db_pool,
        });

        let params: web::Path<(i32, i32)> = web::Path::from((1, 1));
        let response = get_course_detail(app_state, params).await.unwrap();

        assert_eq!(response.status(), StatusCode::OK);
    }
}

如果发生了错误,得到的结果就是 MyError。因为 MyError 实现了 ResponseError trait,Actix 框架会自动转换为错误对应的 HTTP Response。

测试

在终端执行命令 cargo test --bin teacher_service,三个测试都通过了:

在这里插入图片描述

cd 到 webservice,执行命令 cargo run,在浏览器中测试两种错误类型:

在这里插入图片描述

在这里插入图片描述

终端也会打印错误信息:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

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

抵扣说明:

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

余额充值