Skip to main content

bpi_rs/err/
error.rs

1use std::fmt;
2
3use bytes::Bytes;
4use serde::Serialize;
5use thiserror::Error;
6
7/// HTTP 响应模型解码失败时保留的可恢复上下文。
8///
9/// 调试和错误格式化只显示响应长度,不会输出原始响应内容。调用方可以通过
10/// [`BpiError::response_body`] 显式取得响应字节并使用临时模型重新解析。
11pub struct ResponseDecodeError {
12    source: serde_json::Error,
13    body: Bytes,
14}
15
16impl ResponseDecodeError {
17    pub(crate) fn new(source: serde_json::Error, body: Bytes) -> Self {
18        Self { source, body }
19    }
20
21    pub(crate) fn source_error(&self) -> &serde_json::Error {
22        &self.source
23    }
24
25    fn response_body(&self) -> &[u8] {
26        &self.body
27    }
28}
29
30impl fmt::Debug for ResponseDecodeError {
31    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32        formatter
33            .debug_struct("ResponseDecodeError")
34            .field("category", &self.source.classify())
35            .field("line", &self.source.line())
36            .field("column", &self.source.column())
37            .field(
38                "response_body",
39                &format_args!("<redacted: {} bytes>", self.body.len()),
40            )
41            .finish()
42    }
43}
44
45impl fmt::Display for ResponseDecodeError {
46    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(
48            formatter,
49            "failed to decode response ({:?}) at line {} column {}",
50            self.source.classify(),
51            self.source.line(),
52            self.source.column()
53        )
54    }
55}
56
57impl std::error::Error for ResponseDecodeError {
58    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
59        Some(&self.source)
60    }
61}
62
63/// 错误类型分类
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
65pub enum ErrorCategory {
66    /// 权限认证类错误
67    Auth,
68    /// 请求参数类错误
69    Request,
70    /// 服务器类错误
71    Server,
72    /// 业务逻辑类错误
73    Business,
74    /// 网络类错误
75    Network,
76    /// 未知错误
77    Unknown,
78}
79
80#[derive(Debug, Error, Serialize)]
81pub enum BpiError {
82    /// 网络请求失败
83    #[error("网络请求失败: {message}")]
84    Network { message: String },
85
86    /// transport 层请求失败。
87    #[error("transport request failed: {source}")]
88    Transport {
89        #[serde(skip)]
90        source: reqwest::Error,
91    },
92
93    /// HTTP状态码错误
94    #[error("HTTP请求失败,状态码: {status}")]
95    Http { status: u16 },
96
97    /// HTTP 状态错误。
98    #[error("HTTP request failed with status {status}")]
99    HttpStatus { status: u16 },
100
101    /// JSON解析失败
102    #[error("数据解析失败: {message}")]
103    Parse { message: String },
104
105    /// 响应解码失败。
106    #[error("failed to decode response: {source}")]
107    Decode {
108        #[serde(skip)]
109        source: serde_json::Error,
110    },
111
112    /// HTTP 响应模型解码失败;原始响应可供调用方使用临时模型恢复。
113    #[error(transparent)]
114    ResponseDecode {
115        #[serde(skip)]
116        error: ResponseDecodeError,
117    },
118
119    /// API返回的业务错误
120    #[error("API错误 [{code}]: {message}")]
121    Api {
122        code: i32,
123        message: String,
124        category: ErrorCategory,
125    },
126
127    /// 验证错误
128    #[error("验证失败: {message}")]
129    Authentication { message: String },
130
131    /// 认证或授权错误。
132    #[error("authentication failed: {message}")]
133    Auth { message: String },
134
135    /// # 参数错误
136    #[error("参数错误 [{field}]: {message}")]
137    InvalidParameter {
138        field: &'static str,
139        message: &'static str,
140    },
141
142    /// API 响应成功,但未包含必需的 payload 数据。
143    #[error("missing response data")]
144    MissingData,
145
146    /// 当前解析器不支持该响应格式。
147    #[error("unsupported response: {message}")]
148    UnsupportedResponse { message: String },
149}
150
151impl BpiError {
152    pub fn missing_csrf() -> Self {
153        BpiError::InvalidParameter {
154            field: "csrf",
155            message: "缺少CSRF",
156        }
157    }
158
159    pub fn missing_data() -> Self {
160        BpiError::MissingData
161    }
162
163    pub fn auth_required() -> Self {
164        BpiError::Auth {
165            message: "需要登录".to_string(),
166        }
167    }
168
169    pub(crate) fn response_decode(source: serde_json::Error, body: Bytes) -> Self {
170        Self::ResponseDecode {
171            error: ResponseDecodeError::new(source, body),
172        }
173    }
174}
175
176/// 生成Error的From实现
177impl BpiError {
178    /// 根据API错误码创建BpiError
179    pub fn from_code(code: i32) -> Self {
180        let message = super::code::get_error_message(code);
181        let category = super::code::categorize_error(code);
182
183        BpiError::Api {
184            code,
185            message,
186            category,
187        }
188    }
189
190    // 不在错误码表中的API错误
191    pub fn from_code_message(code: i32, message: String) -> Self {
192        let category = super::code::categorize_error(code);
193        BpiError::Api {
194            code,
195            message,
196            category,
197        }
198    }
199
200    /// 从API响应创建BpiError
201    pub fn from_api_response<T>(resp: crate::response::ApiEnvelope<T>) -> Self {
202        if resp.code == 0 {
203            return BpiError::Api {
204                code: 0,
205                message: "API返回成功状态但被当作错误处理".to_string(),
206                category: ErrorCategory::Unknown,
207            };
208        }
209
210        if resp.message.is_empty() || resp.message == "0" {
211            Self::from_code(resp.code)
212        } else {
213            Self::from_code_message(resp.code, resp.message)
214        }
215    }
216}
217
218/// 获取错误属性
219impl BpiError {
220    /// 获取错误码
221    pub fn code(&self) -> Option<i32> {
222        match self {
223            BpiError::Api { code, .. } => Some(*code),
224            _ => None,
225        }
226    }
227
228    /// 获取 HTTP 状态码
229    pub fn http_status(&self) -> Option<u16> {
230        match self {
231            BpiError::Http { status } | BpiError::HttpStatus { status } => Some(*status),
232            _ => None,
233        }
234    }
235
236    /// 返回导致模型解码失败的原始 HTTP 响应。
237    ///
238    /// 只有从 transport 响应反序列化产生的 [`BpiError::ResponseDecode`] 才会返回
239    /// `Some`。普通 JSON 解析错误不会伪装成可恢复的 HTTP 响应错误。
240    pub fn response_body(&self) -> Option<&[u8]> {
241        match self {
242            Self::ResponseDecode { error } => Some(error.response_body()),
243            _ => None,
244        }
245    }
246
247    /// 获取错误分类
248    pub fn category(&self) -> ErrorCategory {
249        match self {
250            BpiError::Api { category, .. } => category.clone(),
251            BpiError::Network { .. } => ErrorCategory::Network,
252            BpiError::Transport { .. } => ErrorCategory::Network,
253            BpiError::Http { .. } => ErrorCategory::Network,
254            BpiError::HttpStatus { .. } => ErrorCategory::Network,
255            BpiError::Parse { .. } => ErrorCategory::Request,
256            BpiError::Decode { .. } => ErrorCategory::Request,
257            BpiError::ResponseDecode { .. } => ErrorCategory::Request,
258            BpiError::InvalidParameter { .. } => ErrorCategory::Request,
259            BpiError::Authentication { .. } => ErrorCategory::Auth,
260            BpiError::Auth { .. } => ErrorCategory::Auth,
261            BpiError::MissingData => ErrorCategory::Request,
262            BpiError::UnsupportedResponse { .. } => ErrorCategory::Request,
263        }
264    }
265}
266
267/// 错误创建函数
268impl BpiError {
269    /// 创建网络错误
270    pub fn network(message: impl Into<String>) -> Self {
271        BpiError::Network {
272            message: message.into(),
273        }
274    }
275
276    /// 创建HTTP错误
277    pub fn http(status: u16) -> Self {
278        BpiError::HttpStatus { status }
279    }
280
281    /// 创建解析错误
282    pub fn parse(message: impl Into<String>) -> Self {
283        BpiError::Parse {
284            message: message.into(),
285        }
286    }
287
288    /// 创建参数错误
289    pub fn invalid_parameter(field: &'static str, message: &'static str) -> Self {
290        BpiError::InvalidParameter { field, message }
291    }
292
293    pub fn auth(message: impl Into<String>) -> Self {
294        BpiError::Auth {
295            message: message.into(),
296        }
297    }
298
299    /// 创建不支持响应错误。
300    pub fn unsupported_response(message: impl Into<String>) -> Self {
301        BpiError::UnsupportedResponse {
302            message: message.into(),
303        }
304    }
305}
306
307/// 错误判断
308impl BpiError {
309    /// 判断是否需要用户登录
310    pub fn requires_login(&self) -> bool {
311        matches!(self.code(), Some(-101) | Some(-401) | Some(800501007))
312            || matches!(self.http_status(), Some(401))
313    }
314
315    /// 判断是否为权限问题
316    pub fn is_permission_error(&self) -> bool {
317        matches!(self.category(), ErrorCategory::Auth)
318            || matches!(self.code(), Some(-403) | Some(-4))
319            || matches!(self.http_status(), Some(403))
320    }
321
322    /// 判断是否需要VIP权限
323    pub fn requires_vip(&self) -> bool {
324        matches!(self.code(), Some(-106) | Some(-650))
325    }
326
327    /// 判断是否为风控拦截
328    pub fn is_risk_control(&self) -> bool {
329        matches!(self.code(), Some(-352) | Some(-412)) || matches!(self.http_status(), Some(412))
330    }
331
332    /// 判断是否为业务逻辑错误
333    pub fn is_business_error(&self) -> bool {
334        matches!(self.category(), ErrorCategory::Business)
335    }
336
337    /// 获取可写入契约的稳定语义错误标签
338    pub fn semantic_error(&self) -> Option<&'static str> {
339        if self.requires_login() {
340            Some("requires_login")
341        } else if self.requires_vip() {
342            Some("requires_vip")
343        } else if self.is_risk_control() {
344            Some("risk_control")
345        } else if self.is_permission_error() {
346            Some("permission_denied")
347        } else if self.is_business_error() {
348            Some("business_error")
349        } else {
350            None
351        }
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use bytes::Bytes;
358
359    use super::*;
360
361    #[test]
362    fn http_status_returns_status_for_legacy_and_current_http_variants() {
363        assert_eq!(BpiError::Http { status: 412 }.http_status(), Some(412));
364        assert_eq!(BpiError::http(403).http_status(), Some(403));
365    }
366
367    #[test]
368    fn requires_login_recognizes_api_and_http_unauthorized_errors() {
369        assert!(BpiError::from_code(-101).requires_login());
370        assert!(BpiError::from_code(800501007).requires_login());
371        assert!(BpiError::http(401).requires_login());
372    }
373
374    #[test]
375    fn is_permission_error_recognizes_api_and_http_forbidden_errors() {
376        assert!(BpiError::from_code(-403).is_permission_error());
377        assert!(BpiError::http(403).is_permission_error());
378    }
379
380    #[test]
381    fn is_risk_control_recognizes_api_and_http_risk_blocks() {
382        assert!(BpiError::from_code(-352).is_risk_control());
383        assert!(BpiError::from_code(-412).is_risk_control());
384        assert!(BpiError::http(412).is_risk_control());
385    }
386
387    #[test]
388    fn semantic_error_returns_stable_contract_labels() {
389        assert_eq!(
390            BpiError::from_code(-101).semantic_error(),
391            Some("requires_login")
392        );
393        assert_eq!(
394            BpiError::from_code(-106).semantic_error(),
395            Some("requires_vip")
396        );
397        assert_eq!(
398            BpiError::from_code(-352).semantic_error(),
399            Some("risk_control")
400        );
401        assert_eq!(
402            BpiError::from_code(-403).semantic_error(),
403            Some("permission_denied")
404        );
405    }
406
407    #[test]
408    fn response_decode_debug_redacts_response_body() {
409        let err = response_decode_error();
410
411        assert!(!format!("{err:?}").contains("private-response-marker"));
412    }
413
414    #[test]
415    fn response_decode_display_redacts_response_body() {
416        let err = response_decode_error();
417
418        assert!(!err.to_string().contains("private-response-marker"));
419    }
420
421    #[test]
422    fn response_decode_serialization_redacts_response_body() -> Result<(), serde_json::Error> {
423        let err = response_decode_error();
424        let serialized = serde_json::to_string(&err)?;
425
426        assert!(!serialized.contains("private-response-marker"));
427        Ok(())
428    }
429
430    fn response_decode_error() -> BpiError {
431        let source = serde_json::from_slice::<u64>(br#""private-response-marker""#).unwrap_err();
432        BpiError::response_decode(
433            source,
434            Bytes::from_static(br#"{"secret":"private-response-marker"}"#),
435        )
436    }
437}