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        matches!(self, HttpError::Reqwest(_))
75    }
76}
77
78/// WeChat SDK error types
79///
80/// # Variants
81///
82/// - `Http`: HTTP request/response errors
83/// - `Json`: JSON serialization/deserialization errors
84/// - `Api`: WeChat API returned an error
85/// - `Token`: Access token related errors
86/// - `Config`: Configuration errors
87/// - `Signature`: Signature verification errors
88/// - `Crypto`: Cryptography operation errors
89/// - `InvalidAppId`: Invalid AppId format
90/// - `InvalidOpenId`: Invalid OpenId format
91/// - `InvalidAccessToken`: Invalid access token
92/// - `InvalidAppSecret`: Invalid AppSecret
93/// - `InvalidSessionKey`: Invalid SessionKey
94/// - `InvalidUnionId`: Invalid UnionId
95#[derive(Debug, Error)]
96pub enum WechatError {
97    /// HTTP request/response error (includes decode errors)
98    #[error("{0}")]
99    Http(HttpError),
100
101    /// JSON serialization/deserialization error
102    #[error("JSON serialization error: {0}")]
103    Json(#[from] serde_json::Error),
104
105    /// WeChat API returned an error
106    ///
107    /// # Fields
108    /// - `code`: Error code returned by WeChat API
109    /// - `message`: Error message from WeChat API
110    #[error("WeChat API error (code={code}): {message}")]
111    Api { code: i32, message: String },
112
113    /// Access token related error
114    #[error("Access token error: {0}")]
115    Token(String),
116
117    /// Configuration error
118    #[error("Configuration error: {0}")]
119    Config(String),
120
121    /// Signature verification failed
122    #[error("Signature verification failed: {0}")]
123    Signature(String),
124
125    /// Cryptography operation error
126    #[error("Crypto operation error: {0}")]
127    Crypto(String),
128
129    /// Invalid AppId format
130    ///
131    /// AppId must start with 'wx' and be 18 characters long
132    #[error("Invalid AppId: {0}")]
133    InvalidAppId(String),
134
135    /// Invalid OpenId format
136    ///
137    /// OpenId must be 20-40 characters
138    #[error("Invalid OpenId: {0}")]
139    InvalidOpenId(String),
140
141    /// Invalid AccessToken
142    #[error("Invalid AccessToken: {0}")]
143    InvalidAccessToken(String),
144
145    /// Invalid AppSecret
146    #[error("Invalid AppSecret: {0}")]
147    InvalidAppSecret(String),
148
149    /// Invalid SessionKey
150    #[error("Invalid SessionKey: {0}")]
151    InvalidSessionKey(String),
152
153    /// Invalid UnionId
154    #[error("Invalid UnionId: {0}")]
155    InvalidUnionId(String),
156}
157
158impl Clone for WechatError {
159    fn clone(&self) -> Self {
160        match self {
161            WechatError::Http(e) => WechatError::Http(e.clone()),
162            WechatError::Json(e) => WechatError::Json(serde_json::Error::io(std::io::Error::new(
163                std::io::ErrorKind::Other,
164                e.to_string(),
165            ))),
166            WechatError::Api { code, message } => WechatError::Api {
167                code: *code,
168                message: message.clone(),
169            },
170            WechatError::Token(msg) => WechatError::Token(msg.clone()),
171            WechatError::Config(msg) => WechatError::Config(msg.clone()),
172            WechatError::Signature(msg) => WechatError::Signature(msg.clone()),
173            WechatError::Crypto(msg) => WechatError::Crypto(msg.clone()),
174            WechatError::InvalidAppId(msg) => WechatError::InvalidAppId(msg.clone()),
175            WechatError::InvalidOpenId(msg) => WechatError::InvalidOpenId(msg.clone()),
176            WechatError::InvalidAccessToken(msg) => WechatError::InvalidAccessToken(msg.clone()),
177            WechatError::InvalidAppSecret(msg) => WechatError::InvalidAppSecret(msg.clone()),
178            WechatError::InvalidSessionKey(msg) => WechatError::InvalidSessionKey(msg.clone()),
179            WechatError::InvalidUnionId(msg) => WechatError::InvalidUnionId(msg.clone()),
180        }
181    }
182}
183
184impl WechatError {
185    /// Check WeChat API response errcode, return error if non-zero.
186    pub(crate) fn check_api(errcode: i32, errmsg: &str) -> Result<(), WechatError> {
187        if errcode != 0 {
188            Err(WechatError::Api {
189                code: errcode,
190                message: errmsg.to_string(),
191            })
192        } else {
193            Ok(())
194        }
195    }
196
197    /// Returns true when this error is safe to retry.
198    pub fn is_transient(&self) -> bool {
199        match self {
200            WechatError::Http(err) => err.is_transient(),
201            WechatError::Api { code, .. } => RETRYABLE_ERROR_CODES.contains(code),
202            _ => false,
203        }
204    }
205}
206
207impl From<reqwest::Error> for WechatError {
208    fn from(e: reqwest::Error) -> Self {
209        WechatError::Http(HttpError::Reqwest(Arc::new(e)))
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use crate::token::RETRYABLE_ERROR_CODES;
217
218    #[test]
219    fn test_invalid_appid_error_message() {
220        let err = WechatError::InvalidAppId("invalid".to_string());
221        assert_eq!(err.to_string(), "Invalid AppId: invalid");
222    }
223
224    #[test]
225    fn test_invalid_openid_error_message() {
226        let err = WechatError::InvalidOpenId("short".to_string());
227        assert_eq!(err.to_string(), "Invalid OpenId: short");
228    }
229
230    #[test]
231    fn test_invalid_access_token_error_message() {
232        let err = WechatError::InvalidAccessToken("".to_string());
233        assert_eq!(err.to_string(), "Invalid AccessToken: ");
234    }
235
236    #[test]
237    fn test_invalid_app_secret_error_message() {
238        let err = WechatError::InvalidAppSecret("wrong".to_string());
239        assert_eq!(err.to_string(), "Invalid AppSecret: wrong");
240    }
241
242    #[test]
243    fn test_invalid_session_key_error_message() {
244        let err = WechatError::InvalidSessionKey("invalid".to_string());
245        assert_eq!(err.to_string(), "Invalid SessionKey: invalid");
246    }
247
248    #[test]
249    fn test_invalid_union_id_error_message() {
250        let err = WechatError::InvalidUnionId("".to_string());
251        assert_eq!(err.to_string(), "Invalid UnionId: ");
252    }
253
254    #[test]
255    fn test_check_api_success() {
256        let result = WechatError::check_api(0, "success");
257        assert!(result.is_ok());
258    }
259
260    #[test]
261    fn test_check_api_error() {
262        let result = WechatError::check_api(40013, "invalid appid");
263        assert!(result.is_err());
264        if let Err(WechatError::Api { code, message }) = result {
265            assert_eq!(code, 40013);
266            assert_eq!(message, "invalid appid");
267        } else {
268            panic!("Expected Api error");
269        }
270    }
271
272    #[test]
273    fn test_wechat_error_clone() {
274        let err = WechatError::Api {
275            code: 40013,
276            message: "invalid appid".to_string(),
277        };
278        let cloned = err.clone();
279        assert_eq!(format!("{}", err), format!("{}", cloned));
280
281        let token_err = WechatError::Token("expired".to_string());
282        let cloned_token = token_err.clone();
283        assert_eq!(format!("{}", token_err), format!("{}", cloned_token));
284    }
285
286    #[test]
287    fn test_http_error_clone() {
288        let err = HttpError::Decode("bad json".to_string());
289        let cloned = err.clone();
290        assert_eq!(format!("{}", err), format!("{}", cloned));
291    }
292
293    #[test]
294    fn test_http_error_source_chain() {
295        use std::error::Error;
296
297        let decode_err = HttpError::Decode("test".to_string());
298        assert!(decode_err.source().is_none());
299    }
300
301    #[test]
302    fn test_http_error_is_transient() {
303        let reqwest_error = reqwest::Client::new().get("http://").build().unwrap_err();
304        let reqwest_http_error = HttpError::Reqwest(Arc::new(reqwest_error));
305        assert!(reqwest_http_error.is_transient());
306
307        let decode_http_error = HttpError::Decode("bad json".to_string());
308        assert!(!decode_http_error.is_transient());
309    }
310
311    #[test]
312    fn test_wechat_error_is_transient_for_http_variants() {
313        let reqwest_error = reqwest::Client::new().get("http://").build().unwrap_err();
314        let transient_error = WechatError::Http(HttpError::Reqwest(Arc::new(reqwest_error)));
315        assert!(transient_error.is_transient());
316
317        let non_transient_error = WechatError::Http(HttpError::Decode("bad json".to_string()));
318        assert!(!non_transient_error.is_transient());
319    }
320
321    #[test]
322    fn test_wechat_error_is_transient_for_api_and_all_other_variants() {
323        for &code in RETRYABLE_ERROR_CODES {
324            let retryable = WechatError::Api {
325                code,
326                message: "retryable".to_string(),
327            };
328            assert!(
329                retryable.is_transient(),
330                "code {} should be transient",
331                code
332            );
333        }
334
335        let non_retryable_api = WechatError::Api {
336            code: 40013,
337            message: "invalid appid".to_string(),
338        };
339        assert!(!non_retryable_api.is_transient());
340
341        let json_error = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
342        let non_transient_variants = [
343            WechatError::Json(json_error),
344            WechatError::Token("token".to_string()),
345            WechatError::Config("config".to_string()),
346            WechatError::Signature("sig".to_string()),
347            WechatError::Crypto("crypto".to_string()),
348            WechatError::InvalidAppId("appid".to_string()),
349            WechatError::InvalidOpenId("openid".to_string()),
350            WechatError::InvalidAccessToken("token".to_string()),
351            WechatError::InvalidAppSecret("secret".to_string()),
352            WechatError::InvalidSessionKey("session".to_string()),
353            WechatError::InvalidUnionId("union".to_string()),
354        ];
355
356        for error in non_transient_variants {
357            assert!(!error.is_transient());
358        }
359    }
360}