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 的转换。
创建自定义错误处理器
- 创建一个自定义错误类型
- 实现 From trait,用于把其它错误类型转化为该类型
- 为自定义错误类型实现 ResponseError trait
- 在 handler 里返回自定义错误类型
- 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,在浏览器中测试两种错误类型:
终端也会打印错误信息: