本教程笔记来自 杨旭老师的 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_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"
}