From 2ab9359161406f99d8c29190fd37aa0756376ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=9E=97=E4=BC=9F?= Date: Fri, 24 Apr 2026 20:23:03 +0800 Subject: [PATCH 1/3] refactor(api): unify request handling and improve error messages --- src/wechat/api.rs | 116 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 90 insertions(+), 26 deletions(-) diff --git a/src/wechat/api.rs b/src/wechat/api.rs index 619e598..638c792 100644 --- a/src/wechat/api.rs +++ b/src/wechat/api.rs @@ -121,23 +121,17 @@ impl WeixinApiClient { headers } - async fn post_json( + async fn request( &self, path: &str, - body: &TReq, + body_provider: impl FnOnce() -> reqwest::RequestBuilder, timeout: Duration, ) -> Result where - TReq: Serialize + ?Sized, TResp: DeserializeOwned, { let url = format!("{}/{}", ILINK_API_ROOT, path); - let body_bytes = serde_json::to_vec(body).context("failed to serialize request body")?; - let response_bytes = self - .client - .post(&url) - .headers(self.json_headers(body_bytes.len())) - .body(body_bytes) + let response_bytes = body_provider() .timeout(timeout) .send() .await @@ -149,6 +143,30 @@ impl WeixinApiClient { Self::decode_response(&response_bytes) } + async fn post_json( + &self, + path: &str, + body: &TReq, + timeout: Duration, + ) -> Result + where + TReq: Serialize + ?Sized, + TResp: DeserializeOwned, + { + let body_bytes = serde_json::to_vec(body).context("failed to serialize request body")?; + let headers = self.json_headers(body_bytes.len()); + let url = format!("{}/{}", ILINK_API_ROOT, path); + + self.request( + path, + || { + self.client.post(&url).headers(headers).body(body_bytes) + }, + timeout, + ) + .await + } + async fn post_form( &self, path: &str, @@ -159,20 +177,14 @@ impl WeixinApiClient { TResp: DeserializeOwned, { let url = format!("{}/{}", ILINK_API_ROOT, path); - let response_bytes = self - .client - .post(&url) - .headers(self.auth_headers()) - .form(form) - .timeout(timeout) - .send() - .await - .with_context(|| format!("error sending request for url ({url})"))? - .bytes() - .await - .with_context(|| format!("error reading response body for url ({url})"))?; - - Self::decode_response(&response_bytes) + self.request( + path, + || { + self.client.post(&url).headers(self.auth_headers()).form(form) + }, + timeout, + ) + .await } fn decode_response(response_bytes: &[u8]) -> Result @@ -182,6 +194,7 @@ impl WeixinApiClient { let status: ApiStatus = serde_json::from_slice(response_bytes).context("failed to decode API status")?; + // Priority 1: errcode (often for session/auth errors) if let Some(code) = status.errcode { if code == SESSION_EXPIRED_ERRCODE { return Err(SessionExpiredError.into()); @@ -195,13 +208,17 @@ impl WeixinApiClient { } } + // Priority 2: ret (often for business logic errors) if let Some(code) = status.ret { if code != 0 { + let message = match code { + -2 => "Invalid context token".to_string(), + -3 => "User ID mismatch or not found".to_string(), + _ => status.err_msg.unwrap_or_else(|| "unknown error".to_string()), + }; return Err(ApiError { code, - message: status - .err_msg - .unwrap_or_else(|| "unknown error".to_string()), + message, } .into()); } @@ -294,4 +311,51 @@ mod tests { assert_eq!(client.bot_token, "tok_123"); assert!(client.route_tag.is_none()); } + + #[test] + fn test_decode_response_success() { + let bytes = b"{}"; + let res: Result = WeixinApiClient::decode_response(bytes); + assert!(res.is_ok()); + } + + #[test] + fn test_decode_response_invalid_token() { + let bytes = b"{\"ret\":-2}"; + let res: Result = WeixinApiClient::decode_response(bytes); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert!(err.to_string().contains("Invalid context token")); + assert!(err.to_string().contains("-2")); + } + + #[test] + fn test_decode_response_wrong_user() { + let bytes = b"{\"ret\":-3}"; + let res: Result = WeixinApiClient::decode_response(bytes); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert!(err.to_string().contains("User ID mismatch or not found")); + assert!(err.to_string().contains("-3")); + } + + #[test] + fn test_decode_response_session_expired() { + let bytes = b"{\"errcode\":-14,\"errmsg\":\"session timeout\"}"; + let res: Result = WeixinApiClient::decode_response(bytes); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert!(is_session_expired(&err)); + assert!(err.to_string().contains("Session expired")); + } + + #[test] + fn test_decode_response_unknown_api_error() { + let bytes = b"{\"ret\":-99, \"err_msg\":\"something went wrong\"}"; + let res: Result = WeixinApiClient::decode_response(bytes); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert!(err.to_string().contains("something went wrong")); + assert!(err.to_string().contains("-99")); + } } From d88692c3bf6bdfd64d4738e0ac38f995cf6f124f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=9E=97=E4=BC=9F?= Date: Fri, 24 Apr 2026 20:27:13 +0800 Subject: [PATCH 2/3] style: run cargo fmt --- src/wechat/api.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/wechat/api.rs b/src/wechat/api.rs index 638c792..a019a72 100644 --- a/src/wechat/api.rs +++ b/src/wechat/api.rs @@ -156,12 +156,10 @@ impl WeixinApiClient { let body_bytes = serde_json::to_vec(body).context("failed to serialize request body")?; let headers = self.json_headers(body_bytes.len()); let url = format!("{}/{}", ILINK_API_ROOT, path); - + self.request( path, - || { - self.client.post(&url).headers(headers).body(body_bytes) - }, + || self.client.post(&url).headers(headers).body(body_bytes), timeout, ) .await @@ -180,7 +178,10 @@ impl WeixinApiClient { self.request( path, || { - self.client.post(&url).headers(self.auth_headers()).form(form) + self.client + .post(&url) + .headers(self.auth_headers()) + .form(form) }, timeout, ) @@ -214,13 +215,11 @@ impl WeixinApiClient { let message = match code { -2 => "Invalid context token".to_string(), -3 => "User ID mismatch or not found".to_string(), - _ => status.err_msg.unwrap_or_else(|| "unknown error".to_string()), + _ => status + .err_msg + .unwrap_or_else(|| "unknown error".to_string()), }; - return Err(ApiError { - code, - message, - } - .into()); + return Err(ApiError { code, message }.into()); } } From b511bfa5b00e0f83e00695d183d4f26de0dddb7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=9E=97=E4=BC=9F?= Date: Fri, 24 Apr 2026 20:33:00 +0800 Subject: [PATCH 3/3] chore: update Cargo.lock --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index e59fc01..76f8edd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1971,7 +1971,7 @@ dependencies = [ [[package]] name = "wechat-cli" -version = "0.4.0" +version = "0.5.0" dependencies = [ "aes", "anyhow",