Skip to main content

wechat_mp_sdk/
error.rs

1//! WeChat SDK error types
2//!
3//! This module defines error types for the WeChat Mini Program SDK.
4//!
5//! ## Common WeChat API Error Codes
6//!
7//! - `-1`: System error
8//! - `0`: Success
9//! - `-1000`: Sign error
10//! - `-1001`: Invalid parameter
11//! - `-1002`: AppID error
12//! - `-1003`: Access token error
13//! - `-1004`: API frequency limit exceeded
14//! - `-1005`: Permission denied
15//! - `-1006`: API call failed
16//! - `40001`: Invalid credential (access_token)
17//! - `40002`: Invalid grant_type
18//! - `40013`: Invalid appid
19//! - `40125`: Invalid appsecret
20
21use std::fmt;
22use std::sync::Arc;
23use thiserror::Error;
24
25use crate::token::RETRYABLE_ERROR_CODES;
26
27/// HTTP/transport error wrapper
28///
29/// Wraps either a reqwest HTTP error or a response decode error.
30#[derive(Debug)]
31pub enum HttpError {
32    /// Reqwest HTTP client error
33    Reqwest(Arc<reqwest::Error>),
34    /// Response body decode error (valid JSON but doesn't match expected type)
35    Decode(String),
36}
37
38impl fmt::Display for HttpError {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            HttpError::Reqwest(e) => write!(f, "{}", e),
42            HttpError::Decode(msg) => write!(f, "Response decode error: {}", msg),
43        }
44    }
45}
46
47impl Clone for HttpError {
48    fn clone(&self) -> Self {
49        match self {
50            HttpError::Reqwest(e) => HttpError::Reqwest(Arc::clone(e)),
51            HttpError::Decode(msg) => HttpError::Decode(msg.clone()),
52        }
53    }
54}
55
56impl std::error::Error for HttpError {
57    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
58        match self {
59            HttpError::Reqwest(e) => Some(e.as_ref()),
60            HttpError::Decode(_) => None,
61        }
62    }
63}
64
65impl From<reqwest::Error> for HttpError {
66    fn from(e: reqwest::Error) -> Self {
67        HttpError::Reqwest(Arc::new(e))
68    }
69}
70
71impl HttpError {
72    /// Returns true when this HTTP error represents a transient transport failure.
73    pub fn is_transient(&self) -> bool {
74        match self {
75            HttpError::Reqwest(error) => match error.status() {
76                Some(status) => status.is_server_error() || status.as_u16() == 429,
77                None => true,
78            },
79            HttpError::Decode(_) => false,
80        }
81    }
82}
83
84/// WeChat SDK error types
85///
86/// # Variants
87///
88/// - `Http`: HTTP request/response errors
89/// - `Json`: JSON serialization/deserialization errors
90/// - `Api`: WeChat API returned an error
91/// - `Token`: Access token related errors
92/// - `Config`: Configuration errors
93/// - `Signature`: Signature verification errors
94/// - `Crypto`: Cryptography operation errors
95/// - `InvalidAppId`: Invalid AppId format
96/// - `InvalidOpenId`: Invalid OpenId format
97/// - `InvalidAccessToken`: Invalid access token
98/// - `InvalidAppSecret`: Invalid AppSecret
99/// - `InvalidSessionKey`: Invalid SessionKey
100/// - `InvalidUnionId`: Invalid UnionId
101#[derive(Debug, Error)]
102pub enum WechatError {
103    /// HTTP request/response error (includes decode errors)
104    #[error("{0}")]
105    Http(HttpError),
106
107    /// JSON serialization/deserialization error
108    #[error("JSON serialization error: {0}")]
109    Json(#[from] serde_json::Error),
110
111    /// WeChat API returned an error
112    ///
113    /// # Fields
114    /// - `code`: Error code returned by WeChat API
115    /// - `message`: Error message from WeChat API
116    #[error("WeChat API error (code={code}): {message}")]
117    Api { code: i32, message: String },
118
119    /// Access token related error
120    #[error("Access token error: {0}")]
121    Token(String),
122
123    /// Configuration error
124    #[error("Configuration error: {0}")]
125    Config(String),
126
127    /// Signature verification failed
128    #[error("Signature verification failed: {0}")]
129    Signature(String),
130
131    /// Cryptography operation error
132    #[error("Crypto operation error: {0}")]
133    Crypto(String),
134
135    /// Invalid AppId format
136    ///
137    /// AppId must start with 'wx' and be 18 characters long
138    #[error("Invalid AppId: {0}")]
139    InvalidAppId(String),
140
141    /// Invalid OpenId format
142    ///
143    /// OpenId must be 20-40 characters
144    #[error("Invalid OpenId: {0}")]
145    InvalidOpenId(String),
146
147    /// Invalid AccessToken
148    #[error("Invalid AccessToken: {0}")]
149    InvalidAccessToken(String),
150
151    /// Invalid AppSecret
152    #[error("Invalid AppSecret: {0}")]
153    InvalidAppSecret(String),
154
155    /// Invalid SessionKey
156    #[error("Invalid SessionKey: {0}")]
157    InvalidSessionKey(String),
158
159    /// Invalid UnionId
160    #[error("Invalid UnionId: {0}")]
161    InvalidUnionId(String),
162}
163
164impl Clone for WechatError {
165    fn clone(&self) -> Self {
166        match self {
167            WechatError::Http(e) => WechatError::Http(e.clone()),
168            WechatError::Json(e) => WechatError::Json(serde_json::Error::io(std::io::Error::new(
169                std::io::ErrorKind::Other,
170                e.to_string(),
171            ))),
172            WechatError::Api { code, message } => WechatError::Api {
173                code: *code,
174                message: message.clone(),
175            },
176            WechatError::Token(msg) => WechatError::Token(msg.clone()),
177            WechatError::Config(msg) => WechatError::Config(msg.clone()),
178            WechatError::Signature(msg) => WechatError::Signature(msg.clone()),
179            WechatError::Crypto(msg) => WechatError::Crypto(msg.clone()),
180            WechatError::InvalidAppId(msg) => WechatError::InvalidAppId(msg.clone()),
181            WechatError::InvalidOpenId(msg) => WechatError::InvalidOpenId(msg.clone()),
182            WechatError::InvalidAccessToken(msg) => WechatError::InvalidAccessToken(msg.clone()),
183            WechatError::InvalidAppSecret(msg) => WechatError::InvalidAppSecret(msg.clone()),
184            WechatError::InvalidSessionKey(msg) => WechatError::InvalidSessionKey(msg.clone()),
185            WechatError::InvalidUnionId(msg) => WechatError::InvalidUnionId(msg.clone()),
186        }
187    }
188}
189
190impl WechatError {
191    /// Check WeChat API response errcode, return error if non-zero.
192    pub(crate) fn check_api(errcode: i32, errmsg: &str) -> Result<(), WechatError> {
193        if errcode != 0 {
194            Err(WechatError::Api {
195                code: errcode,
196                message: errmsg.to_string(),
197            })
198        } else {
199            Ok(())
200        }
201    }
202
203    /// Returns true when this error is safe to retry.
204    pub fn is_transient(&self) -> bool {
205        match self {
206            WechatError::Http(err) => err.is_transient(),
207            WechatError::Api { code, .. } => RETRYABLE_ERROR_CODES.contains(code),
208            _ => false,
209        }
210    }
211}
212
213impl From<reqwest::Error> for WechatError {
214    fn from(e: reqwest::Error) -> Self {
215        WechatError::Http(HttpError::Reqwest(Arc::new(e)))
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use crate::token::RETRYABLE_ERROR_CODES;
223    use wiremock::matchers::{method, path};
224    use wiremock::{Mock, MockServer, ResponseTemplate};
225
226    #[test]
227    fn test_invalid_appid_error_message() {
228        let err = WechatError::InvalidAppId("invalid".to_string());
229        assert_eq!(err.to_string(), "Invalid AppId: invalid");
230    }
231
232    #[test]
233    fn test_invalid_openid_error_message() {
234        let err = WechatError::InvalidOpenId("short".to_string());
235        assert_eq!(err.to_string(), "Invalid OpenId: short");
236    }
237
238    #[test]
239    fn test_invalid_access_token_error_message() {
240        let err = WechatError::InvalidAccessToken("".to_string());
241        assert_eq!(err.to_string(), "Invalid AccessToken: ");
242    }
243
244    #[test]
245    fn test_invalid_app_secret_error_message() {
246        let err = WechatError::InvalidAppSecret("wrong".to_string());
247        assert_eq!(err.to_string(), "Invalid AppSecret: wrong");
248    }
249
250    #[test]
251    fn test_invalid_session_key_error_message() {
252        let err = WechatError::InvalidSessionKey("invalid".to_string());
253        assert_eq!(err.to_string(), "Invalid SessionKey: invalid");
254    }
255
256    #[test]
257    fn test_invalid_union_id_error_message() {
258        let err = WechatError::InvalidUnionId("".to_string());
259        assert_eq!(err.to_string(), "Invalid UnionId: ");
260    }
261
262    #[test]
263    fn test_check_api_success() {
264        let result = WechatError::check_api(0, "success");
265        assert!(result.is_ok());
266    }
267
268    #[test]
269    fn test_check_api_error() {
270        let result = WechatError::check_api(40013, "invalid appid");
271        assert!(result.is_err());
272        if let Err(WechatError::Api { code, message }) = result {
273            assert_eq!(code, 40013);
274            assert_eq!(message, "invalid appid");
275        } else {
276            panic!("Expected Api error");
277        }
278    }
279
280    #[test]
281    fn test_wechat_error_clone() {
282        let err = WechatError::Api {
283            code: 40013,
284            message: "invalid appid".to_string(),
285        };
286        let cloned = err.clone();
287        assert_eq!(format!("{}", err), format!("{}", cloned));
288
289        let token_err = WechatError::Token("expired".to_string());
290        let cloned_token = token_err.clone();
291        assert_eq!(format!("{}", token_err), format!("{}", cloned_token));
292    }
293
294    #[test]
295    fn test_http_error_clone() {
296        let err = HttpError::Decode("bad json".to_string());
297        let cloned = err.clone();
298        assert_eq!(format!("{}", err), format!("{}", cloned));
299    }
300
301    #[test]
302    fn test_http_error_source_chain() {
303        use std::error::Error;
304
305        let decode_err = HttpError::Decode("test".to_string());
306        assert!(decode_err.source().is_none());
307    }
308
309    #[test]
310    fn test_http_error_is_transient() {
311        let reqwest_error = reqwest::Client::new().get("http://").build().unwrap_err();
312        let reqwest_http_error = HttpError::Reqwest(Arc::new(reqwest_error));
313        assert!(reqwest_http_error.is_transient());
314
315        let decode_http_error = HttpError::Decode("bad json".to_string());
316        assert!(!decode_http_error.is_transient());
317    }
318
319    #[test]
320    fn test_wechat_error_is_transient_for_http_variants() {
321        let reqwest_error = reqwest::Client::new().get("http://").build().unwrap_err();
322        let transient_error = WechatError::Http(HttpError::Reqwest(Arc::new(reqwest_error)));
323        assert!(transient_error.is_transient());
324
325        let non_transient_error = WechatError::Http(HttpError::Decode("bad json".to_string()));
326        assert!(!non_transient_error.is_transient());
327    }
328
329    #[tokio::test]
330    async fn test_http_reqwest_status_503_is_transient() {
331        let mock_server = MockServer::start().await;
332        Mock::given(method("GET"))
333            .and(path("/status-503"))
334            .respond_with(ResponseTemplate::new(503))
335            .mount(&mock_server)
336            .await;
337
338        let err = reqwest::Client::new()
339            .get(format!("{}/status-503", mock_server.uri()))
340            .send()
341            .await
342            .unwrap()
343            .error_for_status()
344            .unwrap_err();
345
346        let http_error = HttpError::Reqwest(Arc::new(err));
347        assert!(http_error.is_transient());
348    }
349
350    #[tokio::test]
351    async fn test_http_reqwest_status_400_is_not_transient() {
352        let mock_server = MockServer::start().await;
353        Mock::given(method("GET"))
354            .and(path("/status-400"))
355            .respond_with(ResponseTemplate::new(400))
356            .mount(&mock_server)
357            .await;
358
359        let err = reqwest::Client::new()
360            .get(format!("{}/status-400", mock_server.uri()))
361            .send()
362            .await
363            .unwrap()
364            .error_for_status()
365            .unwrap_err();
366
367        let http_error = HttpError::Reqwest(Arc::new(err));
368        assert!(!http_error.is_transient());
369    }
370
371    #[test]
372    fn test_wechat_error_is_transient_for_api_and_all_other_variants() {
373        for &code in RETRYABLE_ERROR_CODES {
374            let retryable = WechatError::Api {
375                code,
376                message: "retryable".to_string(),
377            };
378            assert!(
379                retryable.is_transient(),
380                "code {} should be transient",
381                code
382            );
383        }
384
385        let non_retryable_api = WechatError::Api {
386            code: 40013,
387            message: "invalid appid".to_string(),
388        };
389        assert!(!non_retryable_api.is_transient());
390
391        let json_error = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
392        let non_transient_variants = [
393            WechatError::Json(json_error),
394            WechatError::Token("token".to_string()),
395            WechatError::Config("config".to_string()),
396            WechatError::Signature("sig".to_string()),
397            WechatError::Crypto("crypto".to_string()),
398            WechatError::InvalidAppId("appid".to_string()),
399            WechatError::InvalidOpenId("openid".to_string()),
400            WechatError::InvalidAccessToken("token".to_string()),
401            WechatError::InvalidAppSecret("secret".to_string()),
402            WechatError::InvalidSessionKey("session".to_string()),
403            WechatError::InvalidUnionId("union".to_string()),
404        ];
405
406        for error in non_transient_variants {
407            assert!(!error.is_transient());
408        }
409    }
410}