Rust Web入门(四):错误处理

本教程笔记来自 杨旭老师的 rust web 全栈教程,链接如下:

https://www.bilibili.com/video/BV1RP4y1G7KF?p=1&vd_source=8595fbbf160cc11a0cc07cadacf22951

学习 Rust Web 需要学习 rust 的前置知识可以学习杨旭老师的另一门教程

https://www.bilibili.com/video/BV1hp4y1k7SV/?spm_id_from=333.999.0.0&vd_source=8595fbbf160cc11a0cc07cadacf22951

项目的源代码可以查看 git:(注意作者使用的是 mysql 数据库而不是原教程的数据库)

https://github.com/aiai0603/rust_web_mysql

今天来入门基于 rust 的 web 怎么样处理错误:

Actix错误处理机制

actix 定义了一个通用的错误类型, actix_web::error::Error , 任何实现了 Error Trait 的类型,都可以通过 ? 运算转化为 Actix 的Error类型

如果在实现了 ResponseError trait 的 Result 中返回 Error,则 actix-web 会将该错误呈现为一个 HTTP 响应,并使用相应的状态码,能实现这个功能的包含了对常见的错误,如 IO错误,Serde 错误,Web 错误等

自定义错误处理器

在内置实现不可用的时候,可以自定义错误到 HTTP Respone。实现 ResponseError 来现实返回一个 HTTP 请求,具体如下:

我们首先自定义一个error.rs 来自定义我们的错误类型,我们创建一个枚举来列举我们可能出现的错误,在定义一个结构来存储我们返回给用户的响应的内容:

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

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),
    #[allow(dead_code)]
    NotFound(String),
}

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

之后我们为 MyError 实现 ResponseError trait ,它包含一些方法,可以在错误发生的时候,将错误转化为一个 Http Respone 返回给用户:

error_response 返回一个给用户的响应信息,而 status_code 返回一个对应的状态码,通过传入一个 error,可以得到返回给用户的响应的具体内容,从而实现错误到 Http Respone 的转换:

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(),
        })
    }
}

因为 error::ResponseError 这个接口还需要我们实现 Debug 和 Display ,所以我们加上 Display 的实现:

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

最后,我们需要将原生的错误类型接入到我们的错误处理中,我们为原生的错误类型实现 From ,当发生原生错误的时候,它就会转化为我们枚举里定义的错误,然后根据我们的代码返回:

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())
    }
}

为项目添加错误处理

现在我们有了一个错误处理的类,我们需要把它用在我们的代码中,因为我们之前说过,任何实现了 Error Trait 的类型,都可以通过 ? 运算转化为 Actix 的 Error 类型,所以我们将之前所有的 unwarp() 替换成 ? 运算即可

同时,我们的返回值就需要改成 Result 类型,成功时返回一个 Ok 包裹数据,失败的时候则通过 ? 运算返回一个 error 类型,而数据库的 error 已经包装进了我们的 MyError 中,所以会自动转化为一个 MyError :

use super::error::MyError;
use crate::models::*;
use sqlx::mysql::MySqlPool;

pub async fn get_courses_for_teacher_db(
    pool: &MySqlPool,
    teacher_id: i32,
) -> Result<Vec<Course>, MyError> {
    let rows = sqlx::query!(
        "SELECT id, teacher_id, name, time
        FROM course
        WHERE teacher_id = ?",
        teacher_id
    )
    .fetch_all(pool)
    .await?;

    let courses: Vec<Course> = rows
        .iter()
        .map(|r| Course {
            id: Some(r.id),
            teacher_id: r.teacher_id,
            name: r.name.clone(),
            time: Some(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 = sqlx::query!(
        "SELECT id, teacher_id, name, time
            FROM course
            WHERE teacher_id = ? and id = ?",
        teacher_id,
        course_id
    )
    .fetch_one(pool)
    .await;

    if let Ok(row) = row {
        Ok(Course {
            id: Some(row.id),
            teacher_id: row.teacher_id,
            name: row.name.clone(),
            time: Some(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<Course, MyError> {
    let data = sqlx::query!(
        "INSERT INTO course ( teacher_id, name)
            VALUES ( ?, ?)",
        new_course.teacher_id,
        new_course.name,
    )
    .execute(pool)
    .await?;

    let row = sqlx::query!(
        "SELECT id, teacher_id, name, time
        FROM course
        WHERE id = ?",
        data.last_insert_id()
    )
    .fetch_one(pool)
    .await?;
    Ok(Course {
        id: Some(row.id),
        teacher_id: row.teacher_id,
        name: row.name.clone(),
        time: Some(row.time.unwrap()),
    })
}

因为我们的数据库方法返回值改变了,所以我们的 handlers 也需要更改,我们使用 map 来处理结果:如果调用数据库成功,收到 OK, map 会将返回值包装成一个 HTTP 响应,如果过程中发生错误,map 会返回一个 error,因为我们已经把相关错误接入了 MyError 这个类型,那么发生的错误就会变成 MyError ,而它实现了 ResponseError 所以会自动转化为一个 HTTP 请求返回给用户:

use super::db_access::*;
use super::error::MyError;
use super::state::AppState;
use actix_web::{web, HttpResponse};

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)
}

use super::models::Course;
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<(usize,)>,
) -> Result<HttpResponse, MyError> {
    let teacher_id = i32::try_from(params.0).unwrap();
    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<(usize, usize)>,
) -> Result<HttpResponse, MyError> {
    let teacher_id = i32::try_from(params.0).unwrap();
    let course_id = i32::try_from(params.1).unwrap();
    get_course_details_db(&app_state.db, teacher_id, course_id)
        .await
        .map(|course| HttpResponse::Ok().json(course))
}

最后我们运行测试用例,如果允许均通过,说明我们的错误处理编写成功。

最后我们启动我们的服务器,然后尝试访问一个不存在 id ,比如 http://localhost:3000/courses/1111 ,如果返给你如下的响应,说明你的项目编写成功了:

{
    "error_message": "Course not found for teacher"
}

你可能感兴趣的:(rust入门,rust,前端,开发语言,后端,错误处理)