#error #axum #error-handling

axum-ctx

Axum 错误处理,受anyhow启发

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 个 包中使用

AGPL-3.0

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_msglog_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 处理器中正确处理 ResultOption

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_elseok_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