feat: enhance HTTP error handling with detailed status code processing
parent
772280da20
commit
babbcb8e03
18
src/error.rs
18
src/error.rs
|
@ -28,6 +28,24 @@ pub enum ValidatorError {
|
||||||
|
|
||||||
#[error("Invalid Google API key format: {0}")]
|
#[error("Invalid Google API key format: {0}")]
|
||||||
KeyFormatInvalid(String),
|
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 {
|
impl From<reqwest::Error> for ValidatorError {
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use backon::{ExponentialBuilder, Retryable};
|
use backon::{ExponentialBuilder, Retryable};
|
||||||
use reqwest::{Client, StatusCode};
|
use reqwest::Client;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::error::ValidatorError;
|
||||||
use crate::types::GeminiKey;
|
use crate::types::GeminiKey;
|
||||||
|
|
||||||
pub async fn send_request(
|
pub async fn send_request(
|
||||||
|
@ -13,7 +14,7 @@ pub async fn send_request(
|
||||||
key: GeminiKey,
|
key: GeminiKey,
|
||||||
payload: &Value,
|
payload: &Value,
|
||||||
max_retries: usize,
|
max_retries: usize,
|
||||||
) -> Result<reqwest::Response, reqwest::Error> {
|
) -> Result<(), ValidatorError> {
|
||||||
let retry_policy = ExponentialBuilder::default()
|
let retry_policy = ExponentialBuilder::default()
|
||||||
.with_max_times(max_retries)
|
.with_max_times(max_retries)
|
||||||
.with_min_delay(Duration::from_secs(1))
|
.with_min_delay(Duration::from_secs(1))
|
||||||
|
@ -27,14 +28,49 @@ pub async fn send_request(
|
||||||
.json(payload)
|
.json(payload)
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.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)
|
.retry(&retry_policy)
|
||||||
.when(|e: &reqwest::Error| {
|
.when(|error: &ValidatorError| {
|
||||||
!matches!(
|
!matches!(
|
||||||
e.status(),
|
error,
|
||||||
Some(StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED)
|
ValidatorError::HttpUnauthorized { .. } | ValidatorError::HttpForbidden { .. }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use reqwest::{Client, IntoUrl, StatusCode};
|
use reqwest::{Client, IntoUrl};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use super::{CACHE_CONTENT_TEST_BODY, GENERATE_CONTENT_TEST_BODY};
|
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]);
|
info!("SUCCESS - {}... - Valid key found", &api_key.as_ref()[..10]);
|
||||||
Ok(api_key)
|
Ok(api_key)
|
||||||
}
|
}
|
||||||
Err(e) => match e.status() {
|
Err(e) => match &e {
|
||||||
Some(
|
ValidatorError::HttpBadRequest { .. }
|
||||||
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN | StatusCode::TOO_MANY_REQUESTS,
|
| ValidatorError::HttpUnauthorized { .. }
|
||||||
) => {
|
| ValidatorError::HttpForbidden { .. }
|
||||||
|
| ValidatorError::HttpTooManyRequests { .. } => {
|
||||||
warn!(
|
warn!(
|
||||||
"INVALID - {}... - {}",
|
"INVALID - {}... - {}",
|
||||||
&api_key.as_ref()[..10],
|
&api_key.as_ref()[..10],
|
||||||
|
@ -40,9 +41,8 @@ pub async fn test_generate_content_api(
|
||||||
Err(ValidatorError::KeyInvalid)
|
Err(ValidatorError::KeyInvalid)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let req_error = ValidatorError::from(e);
|
error!("ERROR- {}... - {}", &api_key.as_ref()[..10], e);
|
||||||
error!("ERROR- {}... - {}", &api_key.as_ref()[..10], req_error);
|
Err(e)
|
||||||
Err(req_error)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -72,10 +72,11 @@ pub async fn test_cache_content_api(
|
||||||
);
|
);
|
||||||
Ok(api_key)
|
Ok(api_key)
|
||||||
}
|
}
|
||||||
Err(e) => match e.status() {
|
Err(e) => match &e {
|
||||||
Some(
|
ValidatorError::HttpBadRequest { .. }
|
||||||
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN | StatusCode::TOO_MANY_REQUESTS,
|
| ValidatorError::HttpUnauthorized { .. }
|
||||||
) => {
|
| ValidatorError::HttpForbidden { .. }
|
||||||
|
| ValidatorError::HttpTooManyRequests { .. } => {
|
||||||
warn!(
|
warn!(
|
||||||
"CACHE INVALID - {}... - {}",
|
"CACHE INVALID - {}... - {}",
|
||||||
&api_key.as_ref()[..10],
|
&api_key.as_ref()[..10],
|
||||||
|
@ -84,13 +85,8 @@ pub async fn test_cache_content_api(
|
||||||
Err(ValidatorError::KeyInvalid)
|
Err(ValidatorError::KeyInvalid)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let req_error = ValidatorError::from(e);
|
error!("CACHE ERROR - {}... - {}", &api_key.as_ref()[..10], e);
|
||||||
error!(
|
Err(e)
|
||||||
"CACHE ERROR - {}... - {}",
|
|
||||||
&api_key.as_ref()[..10],
|
|
||||||
req_error
|
|
||||||
);
|
|
||||||
Err(req_error)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ pub struct ContentPart {
|
||||||
pub role: Option<String>,
|
pub role: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct ThinkingConfig {
|
pub struct ThinkingConfig {
|
||||||
#[serde(rename = "thinkingBudget")]
|
#[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
|
// LazyLock for the cached content test body used in cache API validation
|
||||||
pub static CACHE_CONTENT_TEST_BODY: LazyLock<Value> = LazyLock::new(|| {
|
pub static CACHE_CONTENT_TEST_BODY: LazyLock<Value> = LazyLock::new(|| {
|
||||||
// Generate random text content to meet the minimum 1024 tokens requirement for cache API
|
// 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 {
|
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 {
|
contents: vec![ContentPart {
|
||||||
parts: vec![TextPart { text: long_text }],
|
parts: vec![TextPart { text: long_text }],
|
||||||
role: Some("user".to_string()),
|
role: Some("user".to_string()),
|
||||||
|
|
Loading…
Reference in New Issue