服务器列表错误 服务器出现错误怎么解决
本章涵盖
设置项目结构
Rust 和 Actix Web 中的错误处理基础知识
定义自定义错误处理程序
检索所有课程的错误处理
检索课程详细信息的错误处理
发布新课程时的错误处理
概括
在上一章中,我们编写了通过 API 发布和检索课程的代码。 但我们演示和测试的是快乐路径场景。 然而,在现实世界中,可能会发生多种类型的故障。 数据库服务器可能不可用、请求中提供的导师 ID 可能无效、网络服务器可能出现错误等等。 重要的是,我们的 Web 服务能够检测错误、妥善处理错误并向发送 API 请求的用户或客户端发送有意义的错误消息。 这是通过错误处理来完成的,这是本章的重点。 错误处理不仅对于我们网络服务的稳定性很重要,而且对于提供良好的用户体验也很重要。
图 5.1。 统一 Rust 中的错误处理
图 5.1 总结了我们将在本章中采用的错误处理方法。 我们将向 Web 服务添加自定义错误处理,以统一应用程序中可能遇到的不同类型的错误。 结果是,每当服务器代码执行中出现无效请求或意外故障时,客户端都会收到有意义且适当的 HTTP 状态代码和错误消息。 为了实现这一目标,我们将结合使用 Rust 的核心错误处理功能和 Actix 提供的功能,同时为我们的应用程序自定义错误处理。
5.1 设置项目结构
我们将使用前一章中构建的代码作为添加错误处理的起始基础。 如果您一直在遵循,您可以使用第 4 章中您自己的代码来开始添加错误处理。 或者,克隆以下存储库:https://github.com/peshwar9/rust-servers-services-apps 并使用第 4 章中的迭代 3 的代码作为起点。 我们将在本章中构建代码作为迭代 4,因此首先转到项目根目录 (ezytutors/tutor-db) 并在 src 下创建一个新文件夹 iter4。
本节的代码将组织如下:
src/bin/iter4.rs :main() 函数。
src/iter4/routes.rs:包含路由。
src/iter4/handlers.rs:处理函数。
src/iter4/models.rs:表示课程和实用方法的数据结构。
src/iter4/state.rs:应用程序状态,包含注入到应用程序执行的每个线程中的依赖项。
src/iter4/db_access.rs:为了模块化,数据库访问代码从处理函数中分离出来
src/iter4/errors.rs:自定义错误数据结构和相关的错误处理函数
在列出的文件中,
与第 4 章相比,routes.rs、models.rs 或 state.rs 的源代码不会发生任何变化。
对于 handlers.rs 和 db_access.rs,我们可以从第 4 章中的相应代码开始,但我们将修改它们以合并自定义错误处理。
error.rs 是我们将添加的新源文件。
项目结构应类似于图 5.1 所示
图 5.2。 项目结构
我们还按照以下步骤为本章创建一个新版本的数据库表:
将上一章中的database.sql脚本修改为如下所示:
/* Drop table if it already exists*/drop table if exists ezy_course_c5;/* Create a table. *//* Note: Don't put a comma after last field */create table ezy_course_c5( course_id serial primary key, tutor_id INT not null, course_name varchar(140) not null, posted_time TIMESTAMP default now());/* Load seed data for testing */insert into ezy_course_c5 (course_id,tutor_id, course_name,posted_time)values(1, 1, 'First course', '2021-03-17 05:40:00');insert into ezy_course_c5 (course_id, tutor_id, course_name,posted_time)values(2, 1, 'Second course', '2021-03-18 05:45:00');
请注意,我们对上一章的脚本所做的主要更改是将表的名称从 ezy_course_c4 更改为 ezy_course_c5。
从命令行运行脚本(如图所示)以创建表并加载示例数据:
psql -U <user-name> -d ezytutors < database.sql
确保提供database.sql 文件的正确路径,并在出现提示时输入密码。
创建新表后,我们需要为数据库用户授予该新表的权限。 从终端命令行运行以下命令。
psql -U <user-name> -d ezytutors // Login to psql shellGRANT ALL PRIVILEGES ON TABLE __ezy_course_c5__ to <user-name>\q // Quit the psql shell
将 <用户名> 替换为您自己的用户名,然后执行命令。
编写main()函数:将上一章中的src/bin/iter3.rs复制到本章的项目目录中的src/bin/iter4.rs下,并用iter4修改对iter3的引用。 iter4.rs 的最终代码应如下所示:
use actix_web::{web, App, HttpServer};use dotenv::dotenv;use sqlx::postgres::PgPool;use std::env;use std::io;use std::sync::Mutex;#[path = "../iter4/db_access.rs"] #1mod db_access;#[path = "../iter4/errors.rs"]mod errors;#[path = "../iter4/handlers.rs"]mod handlers;#[path = "../iter4/models.rs"]mod models;#[path = "../iter4/routes.rs"]mod routes;#[path = "../iter4/state.rs"]mod state;use routes::*;use state::AppState;#[actix_rt::main]async fn main() -> io::Result<()> { dotenv().ok(); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); let db_pool = PgPool::connect(&database_url).await.unwrap(); // Construct App State let shared_data = web::Data::new(AppState { health_check_response: "I'm good. You've already asked me ".to_string(), visit_count: Mutex::new(0), db: db_pool, }); //Construct app and configure routes let app = move || { App::new() .app_data(shared_data.clone()) .configure(general_routes) .configure(course_routes) }; //Start HTTP server let host_port = env::var("HOST_PORT").expect("HOST:PORT address is not set in .env file"); HttpServer::new(app).bind(&host_port)?.run().await}
还要确保在 .env 文件中添加数据库访问和服务器端口号的环境变量。
通过运行服务器来进行健全性检查:
cargo run --bin iter4
这是第 3 章的结束状态,但被重新创建为第 4 章的起点。
现在让我们快速浏览一下 Rust 中错误处理的基础知识,然后我们可以将其用于为我们的 Web 服务设计自定义错误处理。
5.2 Rust 和 Actix Web 中的错误处理基础知识
一般来说,编程语言使用两种方法之一进行错误处理——异常处理或返回值。 Rust 使用后者。 这与 Java、Python 或 Javascript 等使用异常处理的语言不同。 在 Rust 中,错误处理被视为语言提供的可靠性保证的推动者,因此 Rust 希望程序员显式地处理错误而不是抛出异常。 为了实现这一目标,有可能失败的 Rust 函数会返回一个 Result 枚举类型,其定义如下所示:
enum Result<T, E> { Ok(T), Err(E),}
Rust 函数签名将包含 Result<T,E> 类型的返回值,其中 T 是成功情况下返回的值的类型,E 是失败情况下返回的错误值的类型。 失败。 结果类型基本上是一种表示计算或函数可以返回两种可能结果之一的方式:计算成功时返回一个值,计算失败时返回一个错误。
让我们看一个例子。 这是一个简单的函数,它将字符串解析为整数,对其进行平方并返回 i32 类型的值。 如果解析失败,则返回 ParseIntError 类型的错误。
fn square(val: &str) -> Result<i32, ParseIntError> { match val.parse::<i32>() { Ok(num) => Ok(i32::pow(num, 2)), Err(e) => Err(e), }}
请注意,Rust 标准库的解析函数返回一个 Result 类型,我们使用 match 语句对其进行解包(即从中提取值)。 请注意此函数的返回值,其模式为 Result<T,E>,在本例中,T 是 i32,E 是 ParseIntError。
让我们编写一个调用 square() 函数的 main() 函数。 这是完整的代码:
use std::num::ParseIntError;fn main() { println!("{:?}", square("2")); println!("{:?}", square("INVALID"));}fn square(val: &str) -> Result<i32, ParseIntError> { match val.parse::<i32>() { Ok(num) => Ok(i32::pow(num, 2)), Err(e) => Err(e), }}
运行此代码,您将看到以下输出打印到控制台。
Ok(4)Err(ParseIntError { kind: InvalidDigit })
在第一种情况下,square() 函数能够成功解析字符串中的数字 2,并返回 Ok() 枚举类型中包含的平方值。 在第二种情况下,会返回 ParseIntError 类型的错误,因为 parse() 函数无法从字符串中提取数字。
现在让我们看一下 Rust 提供的一个特殊运算符,它可以使错误处理更加简洁,即 ? 操作员。 请注意,在前面的代码中,我们使用了 match 子句来解包从 parse() 方法返回的 Result 类型。 接下来让我们看看 ? 的用法 减少样板代码的运算符:
use std::num::ParseIntError;fn main() { println!("{:?}", square("2")); println!("{:?}", square("INVALID"));}fn square(val: &str) -> Result<i32, ParseIntError> { let num = val.parse::<i32>()?; Ok(i32::pow(num,2))}
您会注意到带有关联子句的 match 语句已被替换为 ? 操作员。 该运算符尝试从 Result 值中解开整数并将其存储在 num 变量中。 如果不成功,它会从 parse() 方法接收错误,中止 square 函数并将 ParseIntError 传播到调用函数(在我们的例子中是 main() 函数)。
现在,我们将通过向 square() 函数添加附加功能来探索 Rust 中的错误处理。 此处的代码显示了用于打开文件并将计算出的平方值写入其中的附加代码行。
use std::fs::File;use std::io::Write;use std::num::ParseIntError;fn main() { println!("{:?}", square("2")); println!("{:?}", square("INVALID"));}fn square(val: &str) -> Result<i32, ParseIntError> { let num = val.parse::<i32>()?; let mut f = File::open("fictionalfile.txt")?; let string_to_write = format!("Square of {} is {}", num, i32::pow(num, 2)); f.write_all(string_to_write.as_bytes())?; Ok(i32::pow(num, 2))}
当您编译此代码时,您将收到如下错误消息:
the trait `std::convert::From<std::io::Error>` is not implemented for `std::num::ParseIntError`
错误消息可能看起来令人困惑,但它试图说明的是 File::open 和 write_all 方法返回一个 Result 类型,其中包含 std::io::Error 类型的错误,该错误应该传播回 main () 函数,正如我们使用过的 ? 操作员。 然而,square() 的函数签名明确指出它返回 ParseIntError 类型的错误。 现在我们似乎遇到了一个问题,因为函数可以返回两种可能的错误类型 - std::num::ParseIntError 和 std::io::Error,但我们的函数签名只能指定一种错误类型。
这就是自定义错误类型的用武之地。让我们定义一个自定义错误类型,它可以是 ParseIntError 和 io::Error 类型的抽象。 修改代码如图:
use std::fmt;use std::fs::File;use std::io::Write;#[derive(Debug)]pub enum MyError { #1 ParseError, IOError,}impl std::error::Error for MyError {} #2impl fmt::Display for MyError { #3 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { MyError::ParseError => write!(f, "Parse Error"), MyError::IOError => write!(f, "IO Error"), } }}fn main() { let result = square("INVALID"); match result { #4 Ok(res) => println!("Result is {:?}",res), Err(e) => println!("Error in parsing: {:?}",e) };}fn square(val: &str) -> Result<i32, MyError> { let num = val.parse::<i32>().map_err(|_| MyError::ParseError)?; #5 let mut f = File::open("fictionalfile.txt").map_err(|_| MyError::IOError)?; #6 let string_to_write = format!("Square of {:?} is {:?}", num, i32::pow(num, 2)); f.write_all(string_to_write.as_bytes()) .map_err(|_| MyError::IOError)?; #7 Ok(i32::pow(num, 2))}
我们正在取得进展。 到目前为止我们已经了解了 Rust 如何使用 Result 类型返回错误,我们如何使用 ? 操作符减少传播错误的样板代码,以及如何定义和实现自定义错误类型以统一函数或应用程序级别的错误处理。
Rust 的错误处理使代码安全
Rust 函数可以属于 Rust 标准库、外部包,也可以是程序员编写的自定义函数。 每当可能出现错误时,Rust 函数都会返回 Result 数据类型。 然后,调用函数必须通过 a) 使用 ? 将错误进一步传播给其调用者来处理错误。 运算符,b) 在冒泡之前将收到的任何错误转换为另一种类型,c) 使用匹配块处理 Result::Ok 和 Result::Error 变体,d) 或简单地使用 .unwrap() 或 .expect 对错误进行恐慌 ()。 这使得程序更安全,因为不可能访问从 Rust 函数返回的无效、空或未初始化的数据。
现在让我们看一下 Actix-web 如何构建在 Rust 错误处理理念之上,以返回 Web 服务和应用程序的错误。
图 5.3。 将错误转换为 HTTP 响应
Actix-web 有一个通用错误结构 actix_web::error::Error ,与任何其他 Rust 错误类型一样,它实现了 Rust 标准库的错误特征 std::error::Error。 任何实现 Rust 标准库错误特征的错误类型都可以使用 ? 转换为 Actix 错误类型。 操作员。 然后,Actix 错误类型将自动转换为 HTTP 响应消息,返回到 HTTP 客户端。
下面是返回 Result 类型的基本 Actix 处理函数的示例。
使用cargo new 创建一个新的cargo 项目,并将以下内容添加到Cargo.toml 中的依赖项中:
[dependencies]actix-web = "3"
将以下代码添加到 src/main.rs 中:
use actix_web::{error::Error, web, App, HttpResponse, HttpServer};async fn hello() -> Result<HttpResponse, Error> { #1 Ok(HttpResponse::Ok().body("Hello there!")) #2}#[actix_web::main]async fn main() -> std::io::Result<()> { HttpServer::new(|| App::new().route("/hello", web::get().to(hello))) .bind("127.0.0.1:3000")? .run() .await}
尽管处理函数签名指定它可以返回 Error 类型,但处理函数非常简单,因此这里出现任何问题的可能性很小。
运行程序:
cargo run
从浏览器中,使用以下命令连接到 hello 路由:
http://localhost:3000/hello
您应该会在浏览器屏幕中看到以下消息:
Hello there!
现在更改处理程序函数以包含可能失败的操作。
use actix_web::{error::Error, web, App, HttpResponse, HttpServer};use std::fs::File;use std::io::Read;async fn hello() -> Result<HttpResponse, Error> { let _ = File::open("fictionalfile.txt")?; #1 Ok(HttpResponse::Ok().body("File read successfully")) #2}#[actix_web::main]async fn main() -> std::io::Result<()> { HttpServer::new(|| App::new().route("/hello", web::get().to(hello))) .bind("127.0.0.1:3000")? .run() .await}
再次运行该程序,并从浏览器连接到 hello 路由。 您应该看到以下消息(或类似消息):
No such file or directory (os error 2)
对于敏锐的读者来说,两个直接问题可能会浮现在脑海中:
文件操作返回 std::io::Error 类型的错误,如前面的示例所示。 当函数签名中指定的返回类型为 actix_web::error::Error 时,如何从处理函数发送 std::io::Error 类型的错误?
当我们从处理函数返回错误类型时,浏览器如何显示文本错误消息?
为了回答第一个问题,任何实现 std::error::Error 特征(std::io::Error 所做的)的东西都可以转换为 actix_web::error::error 类型,因为 Actix 框架实现了 std ::error::Error 其自身类型 actix_web::error::error 的特征。 这允许在 std::io::Error 类型上使用问号 (?) 将其转换为 actix_web::error::error 类型。 如需参考,请参阅此链接(或您阅读本文时可用的本文档的更高版本):https://docs.rs/actix-web/3.3.2/actix_web/error/struct.Error.html 。
为了回答第二个问题,任何实现 Actix Web ResponseError 特征的东西都可以转换为 HTTP 响应。 有趣的是,Actix-web 框架包含许多常见错误类型的此特征的内置实现,std::io::Error 就是其中之一。 有关可用默认实现的更多详细信息,请参阅此链接(或您阅读本文时可用的本文档的更高版本):https://docs.rs/actix-web/3.3.2/actix_web/error /trait.ResponseError.html。 Actix 错误类型和 ResponseError 特征的组合为 Actix 的 Web 服务和应用程序提供了大量错误处理支持。
回到我们的示例,当 hello() 处理函数中引发 std::io::Error 类型的错误时,它最终会转换为 HTTP 响应消息。
在本章中,我们将利用 Actix web 的这些功能将自定义错误类型转换为 HTTP 响应消息。
有了这个背景,您现在就可以开始在导师 Web 服务中实施错误处理了。
5.3 定义自定义错误处理程序
在本节中,我们将为 Web 服务定义自定义错误类型。 在此之前,我们先定义一下总体方法:
定义自定义错误枚举类型,封装您期望在 Web 服务中遇到的各种类型的错误。
实现 From 特征(来自 Rust 标准库),将其他不同的错误类型转换为您的自定义错误类型。
为自定义错误类型实现 Actix ResponseError 特征。 这使得 Actix 能够将自定义错误转换为 HTTP 响应。
在应用程序代码(例如处理程序函数)中,返回自定义错误类型,而不是标准 Rust 错误类型或 Actix 错误类型。
没有第 5 步。只需坐下来观看 Actix 自动将从处理程序函数返回的任何自定义错误转换为有效的 HTTP 响应,然后发送回客户端。
图 5.4 说明了这些步骤。
图 5.4。 编写自定义错误类型的步骤
就是这样。 让我们首先创建一个新文件 src/iter4/errors.rs。 我们将分三部分添加该文件的代码。 这是第 1 部分的代码。
清单 5.1。 错误处理 - 第 1 部分
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 EzyTutorError { #1 DBError(String), ActixError(String), NotFound(String),}#[derive(Debug, Serialize)]pub struct MyErrorResponse { #2 error_message: String,}
我们定义了两种用于错误处理的数据结构 - EzyTutorError(Web 服务中的主要错误处理机制)和 MyErrorResponse(面向用户的消息)。 为了在发生错误时将前者转换为后者,我们在 EzyTutorError 的 impl 块中编写一个方法。
实现
回想一下,Rust 的 impl 块是允许开发人员指定与数据类型关联的函数的方式。 这是 Rust 中定义可以在方法调用语法中的类型实例上调用的函数的唯一方法。 例如 如果 Foo 是数据类型,foo 是 Foo 的实例,bar() 是 Foo 的 impl 块中定义的函数,则可以在实例 foo 上调用函数 bar(),如下所示:foo.bar()。 Impl 块还用于将与用户定义的数据类型相关的功能分组在一起,这使得它们更容易发现和代码维护。 此外,Impl 块允许创建关联函数,这些函数基本上是与数据类型关联的函数,而不是与数据类型的实例关联的函数。 例如,要创建 Foo 的新实例,可以定义关联函数 new(),以便 Foo:new() 创建 Foo 的新实例。
清单 5.2。 错误处理 - 第 2 部分*
impl EzyTutorError { fn error_response(&self) -> String { match self { EzyTutorError::DBError(msg) => { println!("Database error occurred: {:?}", msg); "Database error".into() } EzyTutorError::ActixError(msg) => { println!("Server error occurred: {:?}", msg); "Internal server error".into() } EzyTutorError::NotFound(msg) => { println!("Not found error occurred: {:?}", msg); msg.into() } } }}
我们在自定义错误结构 EzyTutorError 上定义了一个名为 error_response() 的方法。 当我们想要发送一条用户友好的消息来通知用户发生了错误时,将调用此方法。 在这里,我们处理所有三种类型的错误,目的是向用户发送回更简单、友好的错误消息。
到目前为止,我们已经定义了错误数据结构,甚至编写了一种将自定义错误结构转换为用户友好的文本消息的方法。 出现的问题是我们如何将错误从 Web 服务传播到 HTTP 客户端? HTTP Web 服务与客户端通信的唯一方式是通过 HTTP 响应消息,对吗?
因此,缺少一种将服务器中生成的自定义错误转换为相应的 HTTP 响应消息的方法。 我们在前面的示例中已经看到如何使用 actix_web::error::ResponseError 特征来实现此目的。 如果处理程序返回一个也实现 ResponseError 特征的错误,Actix web 会将该错误转换为 HTTP 响应,并带有相应的状态代码。
在我们的例子中,这归结为在 EzyTutorError 结构上实现 ResponseError 特征。 要实现此特征,意味着要实现该特征上定义的两个方法:error_response() 和 status_code。 我们看一下代码:
清单 5.3。 错误处理 - 第 3 部分
impl error::ResponseError for EzyTutorError { fn status_code(&self) -> StatusCode { #1 match self { EzyTutorError::DBError(msg) | EzyTutorError::ActixError(msg) => { StatusCode::INTERNAL_SERVER_ERROR } EzyTutorError::NotFound(msg) => StatusCode::NOT_FOUND, } } fn error_response(&self) -> HttpResponse { #2 HttpResponse::build(self.status_code()).json(MyErrorResponse { error_message: self.error_response(), }) }}
现在我们已经定义了自定义错误类型,接下来我们将其合并到 Web 服务的三个 API 的处理程序和数据库访问代码中。
5.4 检索所有课程的错误处理
在本节中,我们将结合 API 的错误处理来检索导师的课程列表。 让我们关注文件 db_access.rs,它包含数据库访问函数。
将以下导入添加到此文件 (db_access.rs):
use super::errors::EzyTutorError;
super 关键字指的是父作用域(对于 db_access 模块),这是错误模块所在的位置。
让我们看一下函数 get_courses_for_tutor_db 中的一部分现有代码。
let course_rows = sqlx::query!( "SELECT tutor_id, course_id, course_name, posted_time FROM ezy_course_c5 where tutor_id = $1", tutor_id ) .fetch_all(pool) .await.?; .unwrap();
特别注意 unwrap() 方法。 这是 Rust 中处理错误的捷径。 每当数据库操作发生错误时,程序线程就会panic并退出。 Rust 中的 unwrap() 关键字的意思是“如果操作成功,则返回结果,在本例中是课程列表。如果出现错误,只需恐慌并中止程序”。
到目前为止这还不错,因为我们刚刚学习如何构建 Web 服务。 但这不是生产服务的预期行为。 对于数据库访问中的每个错误,我们不能允许程序执行恐慌并退出。 我们想要做的是以某种方式处理错误。 如果我们知道如何处理错误本身,我们就可以在那里做。 否则,我们可以将错误从数据库访问代码传播到调用处理函数,然后该函数可以找出如何处理错误。 为了实现这种传播,我们可以使用问号运算符 (?) 而不是 unwrap() 关键字,如图所示。
let course_rows = sqlx::query!( "SELECT tutor_id, course_id, course_name, posted_time FROM ezy_course_c5 where tutor_id = $1", tutor_id ) .fetch_all(pool) .await?;
请注意,对数据库获取操作的结果进行操作的 .unwrap() 方法现在已替换为问号 (?)。 虽然早期的 unwrap() 操作告诉 Rust 编译器在出现错误时恐慌,但 ? 告诉 Rust 编译器“如果出现错误,将 sqlx 数据库错误转换为另一种错误类型并从函数返回,将错误传播到调用处理函数”。 现在的问题是,问号运算符会将数据库错误转换为什么类型?
我们必须明确这一点。
为了以这种方式传播错误(使用?),我们需要更改数据库方法签名以返回结果类型。 正如我们之前所看到的,结果类型表示发生错误的可能性。 它提供了一种方法来表示任何计算或函数调用中两个可能结果中的一个 - Ok(val) 表示成功,其中 val 是成功计算的结果,或者 Err(err) 表示错误,其中 err 是 计算返回的错误。
在我们的数据库获取函数中,我们将这两种可能的结果定义如下:
返回课程向量 - Vec<Course>,如果数据库访问成功,或者
如果数据库获取失败,则返回 EzyTutorError 类型的错误。
如果我们重新审视等待? 数据库获取操作末尾的表达式,我们可以将其解释为如果数据库访问失败,则将sqlx数据库错误转换为EzyTutorError类型的错误,并从函数返回。 在这种失败情况下,调用处理函数将从数据库访问函数接收回 EzyTutorError 类型的错误。
这是 db_access.rs 中修改后的代码。 这些更改在编号注释中突出显示。
清单 5.4。 检索导师课程的数据库访问方法中的错误处理
pub async fn get_courses_for_tutor_db( pool: &PgPool, tutor_id: i32,) -> Result<Vec<Course>, EzyTutorError> { #1 // Prepare SQL statement let course_rows = sqlx::query!( "SELECT tutor_id, course_id, course_name, posted_time FROM ezy_course_c5 where tutor_id = $1", tutor_id ) .fetch_all(pool) .await?; #2 // Extract result let courses: Vec<Course> = course_rows .iter() .map(|course_row| Course { course_id: course_row.course_id, tutor_id: course_row.tutor_id, course_name: course_row.course_name.clone(), posted_time: Some(chrono::NaiveDateTime::from(course_row.posted_time.unwrap())), }) .collect(); match courses.len() { #3 0 => Err(EzyTutorError::NotFound( "Courses not found for tutor".into(), )), _ => Ok(courses), }}
关于在没有找到有效导师 ID 的课程的情况下返回错误的最后一点可以争论是否真的是一个错误。 不过,我们暂时先把这个论点放在一边,并以此作为练习 Rust 错误处理的另一个机会。
我们还可以更改调用处理函数(在 iter4/handler.rs 中)以合并错误处理。 首先添加以下导入:
use super::errors::EzyTutorError;
修改 get_courses_for_tutor() 函数以返回 Result 类型:
pub async fn get_courses_for_tutor( app_state: web::Data<AppState>, path: web::Path<i32>,) -> Result<HttpResponse, EzyTutorError> { #1 let tutor_id = path.into_inner(); get_courses_for_tutor_db(&app_state.db, tutor_id) #2 .await .map(|courses| HttpResponse::Ok().json(courses)) #3}
看来我们已经完成了检索课程列表的错误处理实现。 编译并运行代码:
cargo run --bin iter4
您会注意到存在编译器错误。
这是因为,对于 ? 运算符要工作,程序中引发的每个错误都应首先转换为 EzyTutorError 类型。 例如,如果使用 sqlx 访问数据库时出现错误,sqlx 将返回 sqlx::error::DatabaseError 类型的错误,而 Actix 不知道如何处理它。 因此,我们必须告诉 Actix 如何将 sqlx 错误转换为我们的自定义错误类型 EzyTutorError。 您真的认为 Actix 会为您做这件事吗? 抱歉,你必须写代码!
此处显示的代码将添加到 iter4/errors.rs 中。
清单 5.5。 实现 EzyTutorError 的 From 和 Display 特征
impl fmt::Display for EzyTutorError { #1 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { write!(f, "{}", self) }}impl From<actix_web::error::Error> for EzyTutorError { #2 fn from(err: actix_web::error::Error) -> Self { EzyTutorError::ActixError(err.to_string()) }}impl From<SQLxError> for EzyTutorError { #3 fn from(err: SQLxError) -> Self { EzyTutorError::DBError(err.to_string()) }}
我们现在已经对数据库访问代码和处理程序代码进行了必要的更改,以纳入检索课程列表的错误处理。 使用以下命令构建并运行代码:
cargo run --bin iter4
从浏览器访问以下 URL:
http://localhost:3000/courses/1
您应该能够看到课程列表。
现在让我们测试一下错误情况。
使用无效的导师 ID 访问 API,如下所示:
http://localhost:3000/courses/10
您应该会在浏览器中看到以下显示内容:
{"error_message":"Courses not found for tutor"}
这正如预期的那样。
现在让我们尝试模拟另一种类型的错误。 这次我们将模拟sqlx数据库访问的错误。
在 .env 文件中,将数据库 URL 更改为无效的用户 ID。 一个例子如下所示:
DATABASE_URL=postgres://invaliduser:trupwd@127.0.0.1:5432/truwitter
重新启动网络服务
cargo run --bin iter4
访问有效的 URL,如下所示:
http://localhost:3000/courses/1
您应该在浏览器中看到以下错误消息:
{"error_message":"Database error"}
让我们花几分钟了解一下这里发生了什么。
当我们提供无效的数据库 URL 时,Web 服务数据库访问函数在收到 API 请求后会尝试从连接池创建连接并运行查询。 此操作失败,并且 sqlx 客户端引发了 sqlx::error::DatabaseError 类型的错误。 由于 error.rs 中的以下 From 特征实现,此错误已转换为我们的自定义错误类型 EzyTutorError
impl From<SQLxError> for EzyTutorError { }
然后,EzyTutorError 类型的错误从 db_access.rs 中的数据库访问函数传播到 handlers.rs 中的处理程序函数。 收到此错误后,处理函数将其进一步传播到 Actix Web 框架
curl -v http://localhost:3000/courses/1
您应该在终端中看到一条消息,类似于此处所示的消息:
GET /courses/1 HTTP/1.1> Host: localhost:3000> User-Agent: curl/7.64.1> Accept: */*>< HTTP/1.1 500 Internal Server Error
回到iter4/errors.rs中的status_code()函数。 您会注意到,对于数据库和 actix 错误,我们返回 StatusCode::INTERNAL_SERVER_ERROR 状态代码,它会转换为 HTML 响应状态代码 500。这与curl 工具生成的输出相匹配。
在继续之前,请确保将 .env 文件中的数据库 URL 用户名更正为正确的值,否则将来的测试将失败。
因此,我们为第一个 API 实现了自定义错误处理。 我们还要确保测试脚本没有被破坏。 按如下方式运行测试:
cargo test --bin iter4
你会发现编译器会抛出错误。 这是因为我们的测试脚本还必须修改才能从处理程序接收错误响应。 对 handlers.rs 中的测试脚本进行更改,如下所示:
清单 5.6。 获取导师所有课程的测试脚本
#[actix_rt::test] async fn get_all_courses_success() { dotenv().ok(); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); let pool: PgPool = PgPool::connect(&database_url).await.unwrap(); let app_state: web::Data<AppState> = web::Data::new(AppState { health_check_response: "".to_string(), visit_count: Mutex::new(0), db: pool, }); let tutor_id: web::Path<i32> = web::Path::from(1); let resp = get_courses_for_tutor(app_state, tutor_id).await.unwrap(); #1 assert_eq!(resp.status(), StatusCode::OK); }
注意:Actix web 不支持使用问号 (?) 运算符传播错误,因此我们必须使用 unwrap() 或 Expect() 从 Result 类型中提取 HTTP 响应。
从命令行重新运行以下命令:
cargo test get_all_courses_success --bin iter4
您应该看到测试成功运行。
您会注意到,在前面的命令中,我们仅运行了特定的测试用例 get_all_courses_success。 如果您使用 Cargo test --bin iter4 运行整个测试套件,您可能会收到类似于以下内容的错误:
DBError("duplicate key value violates unique constraint")
这是因为每次运行测试套件时,都会将 course_id = 3 的新记录插入到表中。 如果第二次运行测试,则此记录插入失败,因为 course_id 是表中的主键,并且不能有两条记录具有相同的 course_id。 在这种情况下,只需登录到 psql shell 并从表 ezy_course_c5 中删除 course_id = 3 的条目即可。
不过还有一个更简单的选择。 您可以使用#[ignore]注释告诉货物测试执行器忽略测试套件中的任何特定测试用例。 您可以指定此注释,如下所示:
#[ignore] #[actix_rt::test] async fn post_course_success() { }
现在,您可以使用 Cargo test --bin iter4 运行整个测试套件,您将在控制台上看到类似的内容:
running 3 teststest handlers::tests::post_course_success ... ignoredtest handlers::tests::get_all_courses_success ... oktest handlers::tests::get_course_detail_test ... oktest result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
您会注意到 post_course_success 测试用例已被忽略,并且其他两个测试已运行。
现在,我们还必须对其他两个 API 执行相同的步骤,即更改数据库访问函数、处理程序方法和测试脚本。
5.5 检索课程详细信息的错误处理
让我们看看为第二个 API 合并错误处理所需的更改,即获取课程详细信息。
以下是 db_access.rs 中更新的数据库访问代码:
清单 5.7。 获取课程详细信息的函数中的错误处理
pub async fn get_course_details_db(pool: &PgPool, tutor_id: i32, course_id: i32) -> Result<Course, EzyTutorError> { #1 // Prepare SQL statement let course_row = sqlx::query!( "SELECT tutor_id, course_id, course_name, posted_time FROM ezy_course_c5 where tutor_id = $1 and course_id = $2", tutor_id, course_id ) .fetch_one(pool) .await; if let Ok(course_row) = course_row { #2 // Execute query Ok(Course { course_id: course_row.course_id, tutor_id: course_row.tutor_id, course_name: course_row.course_name.clone(), posted_time: Some(chrono::NaiveDateTime::from(course_row.posted_time.unwrap())), })} else { Err(EzyTutorError::NotFound("Course id not found".into()))}}
让我们更新处理函数:
pub async fn get_course_details( app_state: web::Data<AppState>, path: web::Path<(i32, i32)>,) -> Result<HttpResponse, EzyTutorError> { #1 let (tutor_id, course_id) = path.into_inner(); get_course_details_db(&app_state.db, tutor_id, course_id) .await .map(|course| HttpResponse::Ok().json(course)) #2}
重新启动网络服务
cargo run --bin iter4
访问有效的 URL,如下所示:
http://localhost:3000/courses/1/2
您将看到像以前一样显示的课程详细信息。 现在尝试访问无效课程 ID 的详细信息:
http://localhost:3000/courses/1/10
您应该在浏览器中看到以下错误消息:
{"error_message":"Course id not found"}
我们还可以更改 handlers.rs 中的测试脚本 async fn get_course_detail_test() 以适应从处理函数返回的错误。
let resp = get_course_details(app_state, parameters).await.unwrap(); #1
使用以下命令运行测试:
cargo test get_course_detail_test --bin iter4
测试应该通过。
接下来,我们将合并发布新课程的错误处理。
5.6 发布新课程的错误处理
我们基本上将遵循与其他两个 API 相同的步骤,即修改数据库访问函数、处理函数和测试脚本。
我们先从db_access.rs中的数据库访问函数开始。
清单 5.8。 发布新课程的数据库访问功能中的错误处理
pub async fn post_new_course_db( pool: &PgPool, new_course: Course,) -> Result<Course, EzyTutorError> { #1 let course_row = sqlx::query!("insert into ezy_course_c5 (course_id,tutor_id, course_name) values ($1,$2,$3) returning tutor_id, course_id,course_name, posted_time", new_course.course_id, new_course.tutor_id, new_course.course_name) .fetch_one(pool) .await?; #2 //Retrieve result Ok(Course { #3 course_id: course_row.course_id, tutor_id: course_row.tutor_id, course_name: course_row.course_name.clone(), posted_time: Some(chrono::NaiveDateTime::from(course_row.posted_time.unwrap())), })}
更新处理函数:
pub async fn post_new_course( new_course: web::Json<Course>, app_state: web::Data<AppState>,) -> Result<HttpResponse, EzyTutorError> { #1 post_new_course_db(&app_state.db, new_course.into()) .await .map(|course| HttpResponse::Ok().json(course)) #2}
最后,更新handlers.rs中的测试脚本async fn post_course_success(),在数据库访问函数的返回值上添加unwrap(),如下所示:
#[actix_rt::test] async fn post_course_success() { /// all code not shown here let resp = post_new_course(course_param, app_state).await.unwrap(); #1 assert_eq!(resp.status(), StatusCode::OK); }
使用以下命令重建并重新启动 Web 服务:
cargo run --bin iter4
从命令行发布新课程:
curl -X POST localhost:3000/courses/ -H "Content-Type: application/json" -d '{"course_id":4, "tutor_id": 1, "course_name":"This is the fourth course!"}'
在浏览器上使用以下 URL 验证是否已添加新课程:
http://localhost:3000/courses/1/4
使用以下命令运行测试:
cargo test --bin iter4
所有三个测试都应该成功通过。
让我们快速回顾一下。 在本章中,您学习了如何将 Web 服务中遇到的不同类型的错误转换为自定义错误类型,以及如何将其转换为 HTTP 响应消息,从而在服务器发生错误时向客户端提供有意义的消息。 在此过程中,您还了解了 Rust 中错误处理的更精细概念,这些概念可以应用于任何 Rust 应用程序。 更重要的是,您现在知道如何优雅地处理故障、向用户提供有意义的反馈以及构建可靠且稳定的 Web 服务。
至此,您还完成了导师 Web 服务的三个 API 的错误处理的实现。 Web 服务由数据库支持,可以处理数据库和 actix 错误,以及用户的无效输入。 恭喜!
5.7 总结
Rust 提供了一种强大且符合人体工程学的错误处理方法,具有 Result 类型、对 Result 类型进行操作的组合函数(例如 map 和 map_err)、使用 unwrap() 和 Expect() 的快速代码原型选项、? 运算符来减少代码样板,以及使用 From 特征将错误从一种错误类型转换为另一种错误类型的能力。
Actix web 构建在 Rust 的错误处理功能之上,包含自己的错误类型和 ResponseError 特征。 这些使 Rust 程序员能够定义自定义错误类型,并让 Actix Web 框架在运行时自动将它们转换为有意义的 HTTP 响应消息,以便发送回 Web 客户端或用户。 此外,Actix web 提供了内置的 From 实现来将 Rust 标准库错误类型转换为 Actix Error 类型,还提供了默认的 ResponseError 特征实现来将 Rust 标准库错误类型转换为 HTTP 响应消息。
在 Actix 中实现自定义错误处理涉及以下步骤:
定义一个数据结构来表示自定义错误类型,
定义自定义错误类型可以采用的可能值(例如,数据库错误、未找到错误等)
在自定义错误类型上实现 ResponseError 特征
实现 From 特征,将各种类型的错误(例如 sqlx 错误或 Actix Web 错误)转换为自定义错误类型
更改数据库访问函数和路由处理函数的返回值,以在出现错误时返回自定义错误类型。 然后,Actix Web 框架将自定义错误类型转换为适当的 HTTP 响应,并将错误消息嵌入到 HTTP 响应的正文中。
在本章中,为导师 Web 服务中的三个 API 中的每一个都合并了自定义错误处理。
我们的导师网络服务现在可以使用成熟的数据库来保存数据,以及强大的错误处理框架,可以随着功能的发展进一步定制。 在下一章中,我们将处理另一个典型的现实情况,即管理团队对产品需求的变化以及用户的附加功能请求。 Rust 能经受住大规模代码重构的考验吗?
切换到下一章来找出答案。