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 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#[derive(Debug, Error)]
102pub enum WechatError {
103 #[error("{0}")]
105 Http(HttpError),
106
107 #[error("JSON serialization error: {0}")]
109 Json(#[from] serde_json::Error),
110
111 #[error("WeChat API error (code={code}): {message}")]
117 Api { code: i32, message: String },
118
119 #[error("Access token error: {0}")]
121 Token(String),
122
123 #[error("Configuration error: {0}")]
125 Config(String),
126
127 #[error("Signature verification failed: {0}")]
129 Signature(String),
130
131 #[error("Crypto operation error: {0}")]
133 Crypto(String),
134
135 #[error("Invalid AppId: {0}")]
139 InvalidAppId(String),
140
141 #[error("Invalid OpenId: {0}")]
145 InvalidOpenId(String),
146
147 #[error("Invalid AccessToken: {0}")]
149 InvalidAccessToken(String),
150
151 #[error("Invalid AppSecret: {0}")]
153 InvalidAppSecret(String),
154
155 #[error("Invalid SessionKey: {0}")]
157 InvalidSessionKey(String),
158
159 #[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 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 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}