1use std::fmt;
2
3use bytes::Bytes;
4use serde::Serialize;
5use thiserror::Error;
6
7pub 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#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
65pub enum ErrorCategory {
66 Auth,
68 Request,
70 Server,
72 Business,
74 Network,
76 Unknown,
78}
79
80#[derive(Debug, Error, Serialize)]
81pub enum BpiError {
82 #[error("网络请求失败: {message}")]
84 Network { message: String },
85
86 #[error("transport request failed: {source}")]
88 Transport {
89 #[serde(skip)]
90 source: reqwest::Error,
91 },
92
93 #[error("HTTP请求失败,状态码: {status}")]
95 Http { status: u16 },
96
97 #[error("HTTP request failed with status {status}")]
99 HttpStatus { status: u16 },
100
101 #[error("数据解析失败: {message}")]
103 Parse { message: String },
104
105 #[error("failed to decode response: {source}")]
107 Decode {
108 #[serde(skip)]
109 source: serde_json::Error,
110 },
111
112 #[error(transparent)]
114 ResponseDecode {
115 #[serde(skip)]
116 error: ResponseDecodeError,
117 },
118
119 #[error("API错误 [{code}]: {message}")]
121 Api {
122 code: i32,
123 message: String,
124 category: ErrorCategory,
125 },
126
127 #[error("验证失败: {message}")]
129 Authentication { message: String },
130
131 #[error("authentication failed: {message}")]
133 Auth { message: String },
134
135 #[error("参数错误 [{field}]: {message}")]
137 InvalidParameter {
138 field: &'static str,
139 message: &'static str,
140 },
141
142 #[error("missing response data")]
144 MissingData,
145
146 #[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
176impl BpiError {
178 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 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 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
218impl BpiError {
220 pub fn code(&self) -> Option<i32> {
222 match self {
223 BpiError::Api { code, .. } => Some(*code),
224 _ => None,
225 }
226 }
227
228 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 pub fn response_body(&self) -> Option<&[u8]> {
241 match self {
242 Self::ResponseDecode { error } => Some(error.response_body()),
243 _ => None,
244 }
245 }
246
247 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
267impl BpiError {
269 pub fn network(message: impl Into<String>) -> Self {
271 BpiError::Network {
272 message: message.into(),
273 }
274 }
275
276 pub fn http(status: u16) -> Self {
278 BpiError::HttpStatus { status }
279 }
280
281 pub fn parse(message: impl Into<String>) -> Self {
283 BpiError::Parse {
284 message: message.into(),
285 }
286 }
287
288 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 pub fn unsupported_response(message: impl Into<String>) -> Self {
301 BpiError::UnsupportedResponse {
302 message: message.into(),
303 }
304 }
305}
306
307impl BpiError {
309 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 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 pub fn requires_vip(&self) -> bool {
324 matches!(self.code(), Some(-106) | Some(-650))
325 }
326
327 pub fn is_risk_control(&self) -> bool {
329 matches!(self.code(), Some(-352) | Some(-412)) || matches!(self.http_status(), Some(412))
330 }
331
332 pub fn is_business_error(&self) -> bool {
334 matches!(self.category(), ErrorCategory::Business)
335 }
336
337 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}