4个版本 (重大更新)
0.4.0 | 2024年5月8日 |
---|---|
0.3.0 | 2024年5月6日 |
0.2.0 | 2024年1月28日 |
0.1.0 | 2023年10月19日 |
#866 in HTTP服务器
每月 32 次下载
在 3 个 包中使用
20KB
175 代码行
axum-ctx
Axum 错误处理,受 anyhow 启发。
➡️ 包含示例的文档 ⬅️
lib.rs
:
Axum 错误处理,受 anyhow
启发
与 anyhow
的比较
假设有一个函数 can_fail
,它返回 Result<T, E>
或 Option<T>
。
使用 anyhow
,你可以做以下操作
use anyhow::{Context, Result};
#
let value = can_fail().context("Error message")?;
对于许多类型的程序,这已经足够了。但对于Web后端,你不仅想要报告错误。你想要返回带有适当HTTP状态码的响应。然后你想要记录错误(使用 tracing
)。这正是 axum-ctx
所做的
// Use a wildcard for the best user experience
use axum_ctx::*;
#
let value = can_fail().ctx(StatusCode::BAD_REQUEST).log_msg("Error message")?;
如果发生错误,用户将收到与您指定的状态码相对应的错误消息 "400 Bad Request"。但您可以用自定义错误消息替换此默认消息,以显示给用户
#
#
let value = can_fail()
.ctx(StatusCode::UNAUTHORIZED)
// Shown to the user
.user_msg("You are not allowed to access this resource!")
// NOT shown to the user, only for the log
.log_msg("Someone tries to pentest you")?;
user_msg
的第二次调用将替换用户错误消息。但多次调用 log_msg
将创建一个回溯
#
#
fn returns_resp_result() -> RespResult<()> {
can_fail().ctx(StatusCode::NOT_FOUND).log_msg("Inner error message")
}
let value = returns_resp_result()
.log_msg("Outer error message")?;
上面的代码将导致以下日志消息
2024-05-08T22:17:53.769240Z INFO axum_ctx: 404 Not Found
0: Outer error message
1: Inner error message
惰性求值
类似于 with_context
,这是 anyhow 提供的,axum-ctx
也支持对 消息 的惰性求值。你只需向 user_msg
或 log_msg
提供一个闭包
#
#
let resource_name = "foo";
let value = can_fail()
.ctx(StatusCode::UNAUTHORIZED)
.user_msg(|| format!("You are not allowed to access the resource {resource_name}!"))
.log_msg(|| format!("Someone tries to access {resource_name}"))?;
.user_msg(format!("…"))
即使 can_fail
没有返回 Err
(或 None
对于选项)也会在堆上创建字符串。 .user_msg(|| format!("…"))
(一个带有两个管道 ||
的闭包)只有当实际上发生 Err
/None
时才会创建字符串。
日志记录
axum-ctx
使用 tracing
进行日志记录。这意味着您需要在程序中首先 初始化一个跟踪订阅者 才能查看 axum-ctx
的日志消息。
axum-ctx
会根据选择的响应状态码自动选择一个 跟踪级别。以下是默认范围映射(状态码小于 100 或大于 999 是不允许的)
状态码 | 级别 |
---|---|
100..400 |
调试 |
400..500 |
信息 |
500..600 |
错误 |
600..1000 |
跟踪 |
您可以使用 change_tracing_level
在程序初始化时更改一个或多个状态码的默认级别
示例
假设您想从数据库中获取所有薪资,然后从 Axum API 返回它们的最大值。
所需步骤
1. 从数据库获取所有薪资。这可能失败,例如,如果数据库不可达。
➡️ 您需要处理一个
Result
2. 确定最高薪资。但如果数据库中没有薪资,则没有最高薪资。
➡️ 您需要处理一个
Option
3. 将最高薪资作为 JSON 返回。
首先,让我们定义一个函数来获取所有薪资
async fn salaries_from_db() -> Result<Vec<f64>, String> {
// Imagine getting this error while trying to connect to the database.
Err(String::from("Database unreachable"))
}
现在,让我们看看如何在 Axum 处理器中正确处理 Result
和 Option
use axum::Json;
use http::StatusCode;
use tracing::{error, info};
#
async fn max_salary() -> Result<Json<f64>, (StatusCode, &'static str)> {
let salaries = match salaries_from_db().await {
Ok(salaries) => salaries,
Err(error) => {
error!("Failed to get all salaries from the DB\n{error}");
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Something went wrong. Please try again later",
));
}
};
match salaries.iter().copied().reduce(f64::max) {
Some(max_salary) => Ok(Json(max_salary)),
None => {
info!("The maximum salary was requested although there are no salaries");
Err((StatusCode::NOT_FOUND, "There are no salaries yet!"))
}
}
}
现在,将上面的代码与下面的代码进行比较,该代码使用 axum-ctx
use axum_ctx::*;
#
async fn max_salary() -> RespResult<Json<f64>> {
salaries_from_db()
.await
.ctx(StatusCode::INTERNAL_SERVER_ERROR)
.user_msg("Something went wrong. Please try again later")
.log_msg("Failed to get all salaries from the DB")?
.iter()
.copied()
.reduce(f64::max)
.ctx(StatusCode::NOT_FOUND)
.user_msg("There are no salaries yet!")
.log_msg("The maximum salary was requested although there are no salaries")
.map(Json)
}
这不是一个很棒的链吗?⛓️ 如果忽略漂亮的格式,它基本上是一行。
用户收到消息“出了点问题。请稍后再试”。在您的终端中,您会看到以下日志消息
2024-05-08T22:17:53.769240Z ERROR axum_ctx: Something went wrong. Please try again later
0: Failed to get all salaries from the DB
1: Database unreachable
“关于 map_or_else
和 ok_or_else
怎么样?”您可能会问。如果您像我一样喜欢链式调用,您可以使用它们,但代码将不会像上面使用 axum_ctx
的代码那样简洁。您可以进行比较
#
#
async fn max_salary() -> Result<Json<f64>, (StatusCode, &'static str)> {
salaries_from_db()
.await
.map_err(|error| {
error!("Failed to get all salaries from the DB\n{error}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"Something went wrong. Please try again later",
)
})?
.iter()
.copied()
.reduce(f64::max)
.ok_or_else(|| {
info!("The maximum salary was requested although there are no salaries");
(StatusCode::NOT_FOUND, "There are no salaries yet!")
})
.map(Json)
}
依赖关系
~1.9–2.7MB
~51K SLoC