From babbcb8e03aed77565a91384ec96d291e829adeb Mon Sep 17 00:00:00 2001 From: Yoo1tic <137816438+Yoo1tic@users.noreply.github.com> Date: Sun, 3 Aug 2025 00:42:39 +0800 Subject: [PATCH] feat: enhance HTTP error handling with detailed status code processing --- src/error.rs | 18 ++++++++++++ src/utils/request.rs | 50 ++++++++++++++++++++++++++++----- src/validation/key_validator.rs | 34 ++++++++++------------ src/validation/mod.rs | 5 ++-- 4 files changed, 78 insertions(+), 29 deletions(-) diff --git a/src/error.rs b/src/error.rs index 522520b..8823968 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,6 +28,24 @@ pub enum ValidatorError { #[error("Invalid Google API key format: {0}")] KeyFormatInvalid(String), + + #[error("HTTP 400 Bad Request: {body}")] + HttpBadRequest { body: String }, + + #[error("HTTP 401 Unauthorized: {body}")] + HttpUnauthorized { body: String }, + + #[error("HTTP 403 Forbidden: {body}")] + HttpForbidden { body: String }, + + #[error("HTTP 429 Too Many Requests: {body}")] + HttpTooManyRequests { body: String }, + + #[error("HTTP {status} Client Error: {body}")] + HttpClientError { status: u16, body: String }, + + #[error("HTTP {status} Server Error: {body}")] + HttpServerError { status: u16, body: String }, } impl From for ValidatorError { diff --git a/src/utils/request.rs b/src/utils/request.rs index 8bd3bf4..2004e2b 100644 --- a/src/utils/request.rs +++ b/src/utils/request.rs @@ -1,10 +1,11 @@ use backon::{ExponentialBuilder, Retryable}; -use reqwest::{Client, StatusCode}; +use reqwest::Client; use serde_json::Value; use tokio::time::Duration; use tracing::debug; use url::Url; +use crate::error::ValidatorError; use crate::types::GeminiKey; pub async fn send_request( @@ -13,7 +14,7 @@ pub async fn send_request( key: GeminiKey, payload: &Value, max_retries: usize, -) -> Result { +) -> Result<(), ValidatorError> { let retry_policy = ExponentialBuilder::default() .with_max_times(max_retries) .with_min_delay(Duration::from_secs(1)) @@ -27,14 +28,49 @@ pub async fn send_request( .json(payload) .send() .await?; - debug!("Response for key {}: {:?}", key.as_ref(), response.status()); - response.error_for_status() + + let status = response.status(); + + if status.is_success() { + Ok(()) + } else { + let body = response.text().await.map_err(ValidatorError::from)?; + debug!( + "Response for key {}: status={:?}, body={}", + key.as_ref(), + status, + body + ); + + let status_code = status.as_u16(); + match status_code { + 400 => Err(ValidatorError::HttpBadRequest { body }), + 401 => Err(ValidatorError::HttpUnauthorized { body }), + 403 => Err(ValidatorError::HttpForbidden { body }), + 429 => Err(ValidatorError::HttpTooManyRequests { body }), + 400..=499 => Err(ValidatorError::HttpClientError { + status: status_code, + body, + }), + 500..=599 => Err(ValidatorError::HttpServerError { + status: status_code, + body, + }), + _ => { + // For other status codes, treat as client error + Err(ValidatorError::HttpClientError { + status: status_code, + body, + }) + } + } + } }) .retry(&retry_policy) - .when(|e: &reqwest::Error| { + .when(|error: &ValidatorError| { !matches!( - e.status(), - Some(StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED) + error, + ValidatorError::HttpUnauthorized { .. } | ValidatorError::HttpForbidden { .. } ) }) .await diff --git a/src/validation/key_validator.rs b/src/validation/key_validator.rs index 6426e8f..3616ffe 100644 --- a/src/validation/key_validator.rs +++ b/src/validation/key_validator.rs @@ -1,4 +1,4 @@ -use reqwest::{Client, IntoUrl, StatusCode}; +use reqwest::{Client, IntoUrl}; use tracing::{error, info, warn}; use super::{CACHE_CONTENT_TEST_BODY, GENERATE_CONTENT_TEST_BODY}; @@ -28,10 +28,11 @@ pub async fn test_generate_content_api( info!("SUCCESS - {}... - Valid key found", &api_key.as_ref()[..10]); Ok(api_key) } - Err(e) => match e.status() { - Some( - StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN | StatusCode::TOO_MANY_REQUESTS, - ) => { + Err(e) => match &e { + ValidatorError::HttpBadRequest { .. } + | ValidatorError::HttpUnauthorized { .. } + | ValidatorError::HttpForbidden { .. } + | ValidatorError::HttpTooManyRequests { .. } => { warn!( "INVALID - {}... - {}", &api_key.as_ref()[..10], @@ -40,9 +41,8 @@ pub async fn test_generate_content_api( Err(ValidatorError::KeyInvalid) } _ => { - let req_error = ValidatorError::from(e); - error!("ERROR- {}... - {}", &api_key.as_ref()[..10], req_error); - Err(req_error) + error!("ERROR- {}... - {}", &api_key.as_ref()[..10], e); + Err(e) } }, } @@ -72,10 +72,11 @@ pub async fn test_cache_content_api( ); Ok(api_key) } - Err(e) => match e.status() { - Some( - StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN | StatusCode::TOO_MANY_REQUESTS, - ) => { + Err(e) => match &e { + ValidatorError::HttpBadRequest { .. } + | ValidatorError::HttpUnauthorized { .. } + | ValidatorError::HttpForbidden { .. } + | ValidatorError::HttpTooManyRequests { .. } => { warn!( "CACHE INVALID - {}... - {}", &api_key.as_ref()[..10], @@ -84,13 +85,8 @@ pub async fn test_cache_content_api( Err(ValidatorError::KeyInvalid) } _ => { - let req_error = ValidatorError::from(e); - error!( - "CACHE ERROR - {}... - {}", - &api_key.as_ref()[..10], - req_error - ); - Err(req_error) + error!("CACHE ERROR - {}... - {}", &api_key.as_ref()[..10], e); + Err(e) } }, } diff --git a/src/validation/mod.rs b/src/validation/mod.rs index 6f8ea1d..a6db023 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -19,7 +19,6 @@ pub struct ContentPart { pub role: Option, } - #[derive(Serialize, Deserialize, Debug)] pub struct ThinkingConfig { #[serde(rename = "thinkingBudget")] @@ -66,9 +65,9 @@ pub static GENERATE_CONTENT_TEST_BODY: LazyLock = LazyLock::new(|| { // LazyLock for the cached content test body used in cache API validation pub static CACHE_CONTENT_TEST_BODY: LazyLock = LazyLock::new(|| { // Generate random text content to meet the minimum 1024 tokens requirement for cache API - let long_text = "You are an expert at analyzing transcripts.".repeat(50); + let long_text = "You are an expert at analyzing transcripts.".repeat(150); let cache_request = GeminiRequest { - model: Some("models/gemini-2.5-flash-lite".to_string()), + model: Some("models/gemini-2.5-flash".to_string()), contents: vec![ContentPart { parts: vec![TextPart { text: long_text }], role: Some("user".to_string()),