Skip to main content

tibba_error/
lib.rs

1// Copyright 2026 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use axum::Json;
16use axum::http::{HeaderValue, StatusCode, header};
17use axum::response::{IntoResponse, Response};
18use serde::{Deserialize, Serialize};
19
20/// 不常用的可选字段集合,装箱存放以控制 [`Error`] 的内存占用。
21/// 仅作为 `Error` 内部实现,不对外暴露。
22#[derive(Debug, Default, Serialize, Deserialize, Clone)]
23struct ErrorData {
24    /// 错误子分类,用于在同一 category 下进一步区分错误来源。
25    sub_category: Option<String>,
26    /// 业务错误码,供前端按码处理特定错误。
27    code: Option<String>,
28    /// 是否为需要告警的异常级错误。
29    exception: Option<bool>,
30    /// 附加信息列表,可携带多条上下文说明。
31    extra: Option<Vec<String>>,
32}
33
34// 仅用于将 Error 序列化为扁平 JSON 对象的内部视图。
35#[derive(Serialize)]
36struct ErrorSerialize<'a> {
37    category: &'a str,
38    message: &'a str,
39    #[serde(flatten)]
40    data: &'a ErrorData,
41}
42
43// 仅用于从扁平 JSON 对象反序列化 Error 的内部视图。
44#[derive(Deserialize)]
45struct ErrorDeserialize {
46    #[serde(default)]
47    category: String,
48    #[serde(default)]
49    message: String,
50    #[serde(flatten)]
51    data: ErrorData,
52}
53
54/// 全局 HTTP 错误类型,贯穿整个应用。
55///
56/// 所有字段均为私有,必须通过 [`Error::new`] 创建并经由链式 `with_xxx` /
57/// `add_xxx` 方法配置;读取使用同名 getter([`Error::status`]、
58/// [`Error::category`] 等)。
59///
60/// 内部把可选字段统一装箱到 `Box<ErrorData>`,将 `Result<_, Error>` 的
61/// `Err` 变体保持在 `clippy::result_large_err` 128 字节限制以内。
62#[derive(Debug, Clone, Default)]
63pub struct Error {
64    /// HTTP 状态码,0 表示未显式设置,`IntoResponse` 时回退为 500。
65    status: u16,
66    /// 错误来源模块或分类,如 "cache"、"db"。
67    category: String,
68    /// 面向用户或日志的错误描述信息。
69    message: String,
70    /// 不常用的可选字段,装箱以控制 `Error` 大小。
71    data: Box<ErrorData>,
72}
73
74impl std::fmt::Display for Error {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        f.write_str(&self.message)
77    }
78}
79
80impl std::error::Error for Error {}
81
82/// 序列化为扁平 JSON 对象:`{ category, message, sub_category?, … }`。
83impl Serialize for Error {
84    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
85    where
86        S: serde::Serializer,
87    {
88        ErrorSerialize {
89            category: &self.category,
90            message: &self.message,
91            data: &self.data,
92        }
93        .serialize(serializer)
94    }
95}
96
97impl<'de> Deserialize<'de> for Error {
98    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
99    where
100        D: serde::Deserializer<'de>,
101    {
102        let d = ErrorDeserialize::deserialize(deserializer)?;
103        Ok(Self {
104            status: 0,
105            category: d.category,
106            message: d.message,
107            data: Box::new(d.data),
108        })
109    }
110}
111
112impl Error {
113    /// 以错误信息创建新的 `Error` 实例,其余字段均为默认值。
114    /// `message` 接受任意 `Display` 类型,便于直接由外部错误包装。
115    #[must_use]
116    pub fn new(message: impl ToString) -> Self {
117        Self {
118            message: message.to_string(),
119            ..Default::default()
120        }
121    }
122
123    // ---------- 链式 setter ----------
124
125    /// 设置错误分类(模块来源),支持链式调用。
126    #[must_use]
127    pub fn with_category(mut self, category: impl Into<String>) -> Self {
128        self.category = category.into();
129        self
130    }
131
132    /// 设置错误子分类,支持链式调用。
133    #[must_use]
134    pub fn with_sub_category(mut self, sub_category: impl Into<String>) -> Self {
135        self.data.sub_category = Some(sub_category.into());
136        self
137    }
138
139    /// 设置业务错误码,支持链式调用。
140    #[must_use]
141    pub fn with_code(mut self, code: impl Into<String>) -> Self {
142        self.data.code = Some(code.into());
143        self
144    }
145
146    /// 设置 HTTP 状态码,支持链式调用。
147    #[must_use]
148    pub fn with_status(mut self, status: u16) -> Self {
149        self.status = status;
150        self
151    }
152
153    /// 标记是否为需要告警的异常级错误,支持链式调用。
154    #[must_use]
155    pub fn with_exception(mut self, exception: bool) -> Self {
156        self.data.exception = Some(exception);
157        self
158    }
159
160    /// 追加一条附加上下文信息,支持链式调用。
161    #[must_use]
162    pub fn add_extra(mut self, value: impl Into<String>) -> Self {
163        self.data
164            .extra
165            .get_or_insert_with(Vec::new)
166            .push(value.into());
167        self
168    }
169
170    // ---------- getter ----------
171
172    /// HTTP 状态码;返回 0 表示未显式设置,响应时会回退到 500。
173    pub fn status(&self) -> u16 {
174        self.status
175    }
176
177    /// 错误来源模块。
178    pub fn category(&self) -> &str {
179        &self.category
180    }
181
182    /// 面向用户或日志的错误描述。
183    pub fn message(&self) -> &str {
184        &self.message
185    }
186
187    /// 错误子分类,未设置时返回 `None`。
188    pub fn sub_category(&self) -> Option<&str> {
189        self.data.sub_category.as_deref()
190    }
191
192    /// 业务错误码,未设置时返回 `None`。
193    pub fn code(&self) -> Option<&str> {
194        self.data.code.as_deref()
195    }
196
197    /// 是否为需要告警的异常级错误;未显式设置时返回 `false`。
198    pub fn is_exception(&self) -> bool {
199        self.data.exception.unwrap_or(false)
200    }
201
202    /// 附加上下文列表,未设置时返回空切片。
203    pub fn extra(&self) -> &[String] {
204        self.data.extra.as_deref().unwrap_or(&[])
205    }
206}
207
208/// 将 `Error` 转换为带 JSON 响应体和 `no-cache` 头的 HTTP 响应。
209impl IntoResponse for Error {
210    fn into_response(self) -> Response {
211        let status = StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
212        // 5xx / 异常级错误:响应体隐去可能含内部细节的原始 message 与 extra(如 sqlx / 库
213        // 原始错误文本),只回通用文案;完整 message 仍随 self 存入 extensions 供服务端日志
214        // 读取,不外泄给客户端。category / sub_category / code 属分类信息,保留供前端处理。
215        let mut res = if status.is_server_error() || self.is_exception() {
216            let redacted = ErrorData {
217                sub_category: self.data.sub_category.clone(),
218                code: self.data.code.clone(),
219                exception: self.data.exception,
220                extra: None,
221            };
222            (
223                status,
224                Json(ErrorSerialize {
225                    category: &self.category,
226                    message: "internal server error",
227                    data: &redacted,
228                }),
229            )
230                .into_response()
231        } else {
232            (status, Json(&self)).into_response()
233        };
234        // 把 Error 放入 extensions,方便日志/统计中间件读取上下文
235        res.extensions_mut().insert(self);
236        // 错误响应禁止缓存
237        res.headers_mut()
238            .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
239        res
240    }
241}