引言

作为开发者,我们都经历过这样的场景:项目上线后,你打开日志监控,铺天盖地的 500 Internal Server Error 扑面而来。这些错误像个黑洞,吞噬着你的调试时间,你甚至不知道它们是从数据库查询失败,还是某个第三方 API 调用超时。

更糟的是,这些错误未经处理,直接甩给了前端。用户看到一个冰冷的 500 页面,或者一个包含敏感信息的 JSON 响应,这不仅破坏了用户体验,更暴露了服务器的内部实现。

从异常种类来说,异常的种类有很多,有前端传来的参数不对导致的异常,有数据库连接超时异常,有查询数据查不到需要返回业务异常。不同的异常,我们希望有错误发生时,提供更优雅的响应提示客户。同时,也可以对不同异常设置不同的异常响应码,使前端更方便的处理接口返回值。

Rust 开发 Web 程序时的常见痛点:

  1. 大量的 match 语句,代码变得冗长且难以阅读。
  2. 错误信息不统一,前端拿到一堆难以处理的 500 错误。
  3. 调试困难,服务器内部的详细错误信息没有被记录。
  4. 如何优雅区分服务器异常与业务异常。

我们使用四招来实现优雅处理 Axum Web 应用中的错误处理。

本文相关源码来自本人 Rust Axum 开发 Websocket as a Service 项目。 GitHub 地址:HTTPS://GitHub.com/BruceZhang54110/RTMate

第一招:定义统一接口响应结构

定义一个结构体 RtResponse 统一接口返回结构,代码如下。业务成功时调用 ok_with_data 默认 code 是 200,业务失败时,调用 err 方法。那么如何让系统异常和业务异常都转换为 RtResponse 呢?这时候就要看第二招了。

#[derive(Serialize, Debug)]
pub struct RtResponse<T> {code: i32,message: String,data: Option<T>,
}impl<T> RtResponse<T> {/// 创建一个带数据的业务成功响应pub fn ok_with_data(data: T) -> Self {RtResponse {code: 200,message: 「success」.to_string(),data: Some(data),}}/// 创建一个无数据的业务成功响应pub fn ok() -> Self {RtResponse {code: 200,message: 「success」.to_string(),data: None,}}/// 创建一个业务失败响应pub fn err(code: i32, message: &str) -> Self {RtResponse {code,message: message.to_string(),data: None,}}
}

接口返回的 JSON 结果就会是如下 JSON 格式:

{「code」: 200,「message」: 「success」,「data」: {「app_id」: 「abc」,「token」: 「eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhcHBfaWQiOiJhYmMiLCJjbGllbnRfaWQiOiJlZDU0ZDE2My1iM2EyLTRhZmMtODc4OC01MjAyODA4Yjk0OTEiLCJpYXQiOjE3NTcxNzYwNjgsImV4cCI6MTc1NzE4MzI2OH0.3LNM7jAeG3YL4jb88p4_Ew96gXvw4AoE38MBEYLvK-s」}
}
{「code」: 500,「message」: 「系统内部错误」,「data」: null
}

第二招:统一异常封装

我们不能将数据库错误或文件写入错误信息原封不动的返回给前端,这时需要有一个统一异常处理。因此我们创建一个 ArrError 结构体:

pub struct AppError {pub code: i32,pub message: String,pub source: Option<anyhow::Error>,
}

code:业务错误码,用于前端精确判断错误类型。

message:对前端友好的提示信息。

source:一个 anyhow::Error,用于在日志中打印完整的错误链,这个字段永远不会发送给前端。

Axum 提供了一个用来生成响应结果的 IntoResponse trait,一般情况下,使用 Axum 开发接口时不是必须要实现 IntoResponse trait,如果需要处理程序返回的自定义错误类型,这个时候则有必要使用。在这里实现方法中转换为 RtResponse。

// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {fn into_response(self) -> axum::response::Response {// 使用 () 作为 T,表示没有数据let response = RtResponse::<()> {code: self.code,message: self.message,data: None,};(StatusCode::OK, Json(response)).into_response()}}

第三招:转换服务器异常类型

前面定义了统一异常结构体,这时候就要派上用场了。我们需求是,对于数据库连接失败,文件写入失败这些我们无法预料的错误,我们希望它们都统一返回 500,保证敏感错误信息不展示给前端的同时,在服务端有日志打印可以看到错误异常堆栈信息。

我们可以利用 anyhow 处理不同的错误,因为它能将任何实现了 std::error::Error 的类型封装起来。anyhow 的错误信息如何转换为 AppError 呢,这里我们要用到 From 与 Into,统一转换为 code 是 500, message 是 “系统服务器异常” 的 AppError,代码如下:

impl <E> From<E> for AppError
whereE: Into<anyhow::Error>
{fn from(value: E) -> Self {let source = value.into();tracing::error!(「Internal error: {:?}」, source);AppError {code: 500, // 500 表示服务器异常message: 「系统内部错误」.to_string(),source: Some(source),}}}

在这里有必要讲一下 From trait ,它定义了一个类型定义如何从另一个类型创建自身,从而提供了一种非常简单的在多种类型之间转换的机制。标准库中有许多此 trait 的实现,用于原始类型和常见类型的转换。

例如,我们可以轻松地将 a 转换 str 为 a String

let my_str = 「hello」;
let my_string = String::from(my_str);

Into trait 可以理解为 From trait 的反转,如果一个类型实现了 From,那么编译器会自动为它实现 Into 。

  • From 表示可以从 T 类型转换为实现 From trait 的类型
  • Into 表示某个类型可以转换为 T

所以在我们异常转换的场景中就实现了 From trait ,将其他异常转换为我们统一定义的 AppErrr**。**

impl <E> From<E> for AppError  // AppError 是目标类型
whereE: Into<anyhow::Error> // 这里被转换类型

单元测试:

// 测试 anyhow::Error 是否能正确转换为 AppError#[test]fn test_anyhow_error_to_app_error_conversion() {let anyhow_error = anyhow!(「数据库连接失败」);let app_error = AppError::from(anyhow_error);assert_eq!(app_error.code, 500);assert_eq!(app_error.message, 「系统内部错误」);}

执行结果:

running 1 test
test tests::test_anyhow_error_to_app_error_conversion ... oksuccesses:successes:tests::test_anyhow_error_to_app_error_conversiontest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

这样我们就实现了让它们都统一返回 500,保证敏感错误信息不展示给前端的同时,在服务端有日志打印可以看到错误异常堆栈信息。

第四招:转换自定义异常类型

我使用枚举定义一些业务异常,这些业务异常对前端来说是重要的,所以业务异常 code 和 message 需要和系统异常区分开,让前端清晰的判断业务异常。

pub enum BizError {// 应用未找到AppNotFound,// 参数错误InvalidParams,// 非法签名InvalidSignature,
}

为 AppError 实现 From trait,这样业务异常也能转换为 AppError 了,这里我们使用 match 对于不同的业务异常返回不同的 code 和 message。

impl From<BizError> for AppError {fn from(value: BizError) -> Self {match value {BizError::AppNotFound => AppError {code: 1004,message: 「您的 app 未找到,请检查 appId」.to_string(),source: None,},BizError::InvalidParams => AppError {code: 400,message: 「参数错误」.to_string(),source: None,},BizError::InvalidSignature => AppError {code: 1005,message: 「签名验证失败,请检查您的请求是否合法」.to_string(),source: None,},}}
}

单元测试:

// 测试 BizError::AppNotFound 是否能正确转换为 AppError#[test]fn test_biz_error_to_app_error_conversion() {// 创建一个 BizError 实例let biz_error = BizError::AppNotFound;let app_error = AppError::from(biz_error);assert_eq!(app_error.code, 1004);assert_eq!(app_error.message, 「您的 app 未找到,请检查 appId」);}

执行结果:

running 1 test
test tests::test_biz_error_to_app_error_conversion ... oksuccesses:successes:tests::test_biz_error_to_app_error_conversion

那么如何使用呢 ?

fn get() -> Result<Json<RtResponse<AppAuthResult>>, AppError> {/////// 省略if signature != rt_app_param.signature {// 签名不匹配,返回错误return Err(AppError::from(BizError::InvalidSignature));}/////// 省略Ok(Json(RtResponse::ok_with_data(result)))
}
let rt_app = web_context.dao.get_rt_app_by_app_id(&rt_app_param.app_id).await?.ok_or(BizError::AppNotFound))?;

这里有两点需要注意:

  1. 第一是使用了错误传播运算符? ,如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。如果值是 Err,Err 将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被传播给了调用者。

所以如果 get_rt_app_by_app_id 返回了 Error,错误就会立马返回,而且因为 AppError 实现了 From trait,会自动将错误类型进行转换。到了上方调用者错误就转换为了 AppError。

  1. 第二,如果 get_rt_app_by_app_id 查不到返回 None,在这里使用 了 ok_or 将 None 转换为 Err,参数是自定义的枚举异常。如果查不到 app_id 就会返回 BizError::AppNotFound 业务异常。由于 BizError 实现了 From trait,同样可以转换为 AppError。

方法返回了 AppError 之后,Axum 根据我们实现的 IntoResponse 方法,将 AppError 的 code 和 message 转换为 RtResponse 的 code 和 message,data 是 None。

总结一下,正常结果转换为 RtResponse,业务异常先转换为有业务自定义 code 和 message 的 AppError,AppError 转换为 RtResponse,最后返回给前端响应。如果是系统异常,结合错误传播运算符?,系统异常转换为 AppError,AppError 转换为 RtResponse,最后返回给前端响应。这样我们就以一种简洁优雅的方式统一了错误返回。

初次学习 Axum 开发 Web 应用,虽然 Rust 学习难度大,但是随着学习的深入,发现类似 From trait 这种巧妙的设计,开发出简洁优雅且性能并未打折的代码,还有是小小的爽感的。在学习和写项目过程中,写技术博客帮助自己巩固知识点,后续 Rust 相关技术文章继续更新,欢迎点赞关注。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/98288.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/98288.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/98288.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL高可用方案解析:从复制到云原生

MySQL 的高可用 (High Availability, HA) 方案旨在确保数据库服务在硬件故障、软件崩溃、网络中断或计划维护时仍能持续可用&#xff0c;最小化停机时间&#xff08;通常目标为 99.9% 至 99.999% 可用性&#xff09;。以下是 MySQL 领域成熟且广泛应用的几种主流高可用方案&…

腾讯云语音接口实现会议系统

1.前言 在现代企业协作环境中&#xff0c;高效的会议管理是提升团队生产力的关键。本文将深入解析一个完整的会议管理系统&#xff0c;涵盖从会议创建到总结生成的完整生命周期。该系统构建一个基于AI技术的智能会议系统&#xff0c;实现会议全流程的智能化管理&#xff0c;包括…

【LeetCode 每日一题】1277. 统计全为 1 的正方形子矩阵

Problem: 1277. 统计全为 1 的正方形子矩阵 文章目录整体思路完整代码时空复杂度时间复杂度&#xff1a;O(m * n)空间复杂度&#xff1a;O(m * n)整体思路 这段代码旨在解决一个经典的二维矩阵问题&#xff1a;统计全为 1 的正方形子矩阵个数 (Count Square Submatrices with …

【论文阅读】MedResearcher-R1: 基于知识引导轨迹合成框架的专家级医学深度研究员

论文链接&#xff1a;https://arxiv.org/pdf/2508.14880 【导读】当通用大模型还在“背题库”时&#xff0c;蚂蚁集团联合哈工大推出的 MedResearcher-R1 已把“临床查房”搬进训练场&#xff01;这篇 2025 年 9 月发布的论文&#xff0c;首次让开源 32B 模型在医学深度研究基准…

基于大语言模型的事件响应优化方案探索

程序员的技术管理推荐阅读 当愿望遇上能力鸿沟&#xff1a;一位技术管理者眼中的团队激励思考 从“激励”到“保健”&#xff1a;80后与90后程序员&#xff0c;到底想要什么&#xff1f; 从“激励”到“保健”&#xff1a;80后与90后程序员&#xff0c;到底想要什么&#xff1f…

数字化浪潮下,传统加工厂如何智能化转型?

在制造业向高端化、服务化升级的今天&#xff0c;传统加工厂正面临前所未有的挑战。订单碎片化、人力成本攀升、设备OEE&#xff08;综合效率&#xff09;长期低于50%、质量波动难以追溯……这些痛点不仅压缩着企业利润空间&#xff0c;更让其在应对市场需求变化时显得迟缓。当…

谓语动词选择指南

文章目录谓语动词的重要性谓语动词类别一. 助动词1. be&#xff08;am, is, are, was, were, been, being&#xff09;表示 存在、状态、身份、特征。2. have&#xff08;have, has, had&#xff09;表示 拥有、经历 或 完成时态的助动词。3. do&#xff08;do, does, did&…

代码随想录学习摘抄day7(二叉树11-21)

一个朴实无华的目录题型226.翻转二叉树思路&#xff1a;把每一个节点的左右孩子交换一下101. 对称二叉树思路&#xff1a;使用队列来比较两个树&#xff08;根节点的左右子树&#xff09;是否相互翻转222.完全二叉树的节点个数思路&#xff1a;本题直接就是求有多少个节点&…

Python+DRVT 从外部调用 Revit:批量创建楼板

今天继续批量创建常用的基础元素&#xff1a;楼板。这次以简单的轮廓为矩形的楼板为例。让我们来看一看如何让Revit自动干活&#xff1a; from typing import List import math # drvt_pybind 支持多会话、多文档&#xff0c;先从简单的单会话、单文档开始 # MyContext是在Pyt…

猿辅导数据分析面试题及参考答案

给定用户成绩表,编写SQL查询排名靠前的用户(例如前10名),并说明rank()和dense_rank()的区别。 要查询成绩表中排名靠前的用户(如前10名),需先明确排名依据(通常为成绩降序),再通过排序和限制结果行数实现。假设用户成绩表名为user_scores,包含user_id(用户ID)和s…

在树莓派集群上部署 Distributed Llama (Qwen 3 14B) 详细指南

项目地址&#xff1a;https://github.com/b4rtaz/distributed-llama 本文档将指导您如何使用一个树莓派5作为Root节点和三个树莓派4作为Worker节点&#xff0c;共同搭建一个4节点的分布式LLM推理集群&#xff0c;并运行10.9GB的Qwen 3 14B模型。 中间要用到github和huggingface…

C++ 容器——unordered_xxx

自 C11 开始&#xff0c;STL 引入了基于 hash table 的 unordered_set、unordered_map 等容器&#xff0c;正如其名它们是无序容器。一定数量&#xff08;据说有测试数据是10000000&#xff09;元素时无序容器的性能要比对应的有序容器优。一、容器数据结构unordered_set、unor…

分布式常见面试题整理

一、分布式理论&#xff1a; CAP理论 分布式系统最多同时满足一致性&#xff08;C&#xff09;、可用性&#xff08;A&#xff09;、分区容错性&#xff08;P&#xff09;中的两个&#xff0c;无法三者兼得。 BASE理论 对CAP中一致性和可用性的权衡&#xff0c;强调基本可用&a…

Python基础入门常用198英语单词详解

最近&#xff0c;我总结了一份Python学习者入门常用单词表&#xff0c;列出了Python学习中常见的198个高频单词&#xff0c;供初学者学习使用。 这些单词都比较简单&#xff0c;非常易于理解&#xff0c;在掌握好单词的基础上&#xff0c;再去学Python可以达到事半功倍的效果。…

EP-SPY 網路追蹤規避實驗:山脈通聯測試

EP-SPY V3.0 https://github.com/MartinxMax/ep-spy 基於 GI6E 編碼的無線電通信工具&#xff0c;用於保護您的隱私。 https://github.com/MartinxMax/gi6e 編寫了偽協議以防止內容被解密無法通過網絡追蹤&#xff0c;抵抗官方監控無線音頻廣播&#xff0c;用於隱蔽信息傳輸…

苹果 FoundationModels 秘典侠客行:隐私为先的端侧 AI 江湖

引子 话说侠客岛之上&#xff0c;有一对年轻侠侣 ——「青锋剑客」凌云与「素心仙子」苏凝&#xff0c;二人自幼习武&#xff0c;尤擅拆解各路奇功秘籍。 近日听闻苹果谷&#xff08;Apple&#xff09;于 WWDC 2025 武林大会之上&#xff0c;亮出一门全新绝学「FoundationMod…

华为基于IPD的产品质量计划模板

目录 模板:产品质量计划模板....................................... 1 1. 介绍...................................................................... 5 1.1. 范围和目的.................................................... 5 1.2. 参考资料..…

事务管理的选择:为何 @Transactional 并非万能,TransactionTemplate 更值得信赖

在 Spring 生态的后端开发中&#xff0c;事务管理是保障数据一致性的核心环节。开发者常常会使用 Transactional 注解快速开启事务&#xff0c;一行代码似乎就能解决问题。但随着业务复杂度提升&#xff0c;这种“简单”的背后往往隐藏着难以察觉的隐患。本文将深入剖析 Spring…

CodePerfAI体验:AI代码性能分析工具如何高效排查性能瓶颈、优化SQL执行耗时?

前阵子帮同事排查用户下单接口的性能问题时&#xff0c;我算是真切感受到 “找性能瓶颈比写代码还磨人”—— 接口偶尔会突然卡到 3 秒以上&#xff0c;查日志只看到 “SQL 执行耗时过长”&#xff0c;但具体是哪个查询慢、为什么慢&#xff0c;翻了半天监控也没头绪&#xff0…

《sklearn机器学习——绘制分数以评估模型》验证曲线、学习曲线

估计器的偏差、方差和噪声 每一个估计器都有其优势和劣势。它的泛化误差可以分解为偏差、方差和噪声。估计器的偏差是不同训练集的平均误差。估计器的方差表示对不同训练集&#xff0c;模型的敏感度。噪声是数据的特质。 在下图中&#xff0c;可以看见一个函数 f(x)cos⁡32πxf…