1use std::fmt;
22use std::sync::Arc;
23use thiserror::Error;
24
25use crate::token::RETRYABLE_ERROR_CODES;
26
27#[derive(Debug)]
31pub enum HttpError {
32 Reqwest(Arc<reqwest::Error>),
34 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 pub fn is_transient(&self) -> bool {
74 matches!(self, HttpError::Reqwest(_))
75 }
76}
77
78#[derive(Debug, Error)]
96pub enum WechatError {
97 #[error("{0}")]
99 Http(HttpError),
100
101 #[error("JSON serialization error: {0}")]
103 Json(#[from] serde_json::Error),
104
105 #[error("WeChat API error (code={code}): {message}")]
111 Api { code: i32, message: String },
112
113 #[error("Access token error: {0}")]
115 Token(String),
116
117 #[error("Configuration error: {0}")]
119 Config(String),
120
121 #[error("Signature verification failed: {0}")]
123 Signature(String),
124
125 #[error("Crypto operation error: {0}")]
127 Crypto(String),
128
129 #[error("Invalid AppId: {0}")]
133 InvalidAppId(String),
134
135 #[error("Invalid OpenId: {0}")]
139 InvalidOpenId(String),
140
141 #[error("Invalid AccessToken: {0}")]
143 InvalidAccessToken(String),
144
145 #[error("Invalid AppSecret: {0}")]
147 InvalidAppSecret(String),
148
149 #[error("Invalid SessionKey: {0}")]
151 InvalidSessionKey(String),
152
153 #[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 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 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}