feat: enhance HTTP error handling with detailed status code processing

main
Yoo1tic 2025-08-03 00:42:39 +08:00
parent 772280da20
commit babbcb8e03
4 changed files with 78 additions and 29 deletions

View File

@ -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<reqwest::Error> for ValidatorError {

View File

@ -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<reqwest::Response, reqwest::Error> {
) -> 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

View File

@ -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)
}
},
}

View File

@ -19,7 +19,6 @@ pub struct ContentPart {
pub role: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ThinkingConfig {
#[serde(rename = "thinkingBudget")]
@ -66,9 +65,9 @@ pub static GENERATE_CONTENT_TEST_BODY: LazyLock<Value> = LazyLock::new(|| {
// LazyLock for the cached content test body used in cache API validation
pub static CACHE_CONTENT_TEST_BODY: LazyLock<Value> = 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()),