use std::fmt;
use std::time::Duration;
#[derive(Debug)]
pub struct ProviderError {
pub status: u16,
pub body: String,
pub retry_after_ms: Option<u64>,
provider: String,
}
impl ProviderError {
#[must_use]
pub fn new(
status: u16,
provider: impl Into<String>,
body: impl Into<String>,
retry_after_ms: Option<u64>,
) -> Self {
Self {
status,
provider: provider.into(),
body: body.into(),
retry_after_ms,
}
}
pub async fn from_response(response: reqwest::Response, provider: impl Into<String>) -> Self {
let status = response.status().as_u16();
let retry_after_ms = response
.headers()
.get(reqwest::header::RETRY_AFTER)
.and_then(|v| v.to_str().ok())
.and_then(parse_retry_after_value);
let body = response.text().await.unwrap_or_default();
Self {
status,
provider: provider.into(),
body,
retry_after_ms,
}
}
}
impl fmt::Display for ProviderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} API error ({}): {}",
self.provider, self.status, self.body
)
}
}
impl std::error::Error for ProviderError {}
pub(crate) fn parse_retry_after_value(value: &str) -> Option<u64> {
let num_str: String = value
.trim()
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
if num_str.is_empty() {
return None;
}
let secs = num_str.parse::<f64>().ok()?;
if secs.is_finite() && secs >= 0.0 {
let millis = Duration::from_secs_f64(secs).as_millis();
u64::try_from(millis).ok()
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn provider_error_display_format() {
let err = ProviderError::new(429, "OpenAI", "Rate limit exceeded", None);
let msg = err.to_string();
assert!(msg.contains("OpenAI API error (429)"));
assert!(msg.contains("Rate limit exceeded"));
}
#[test]
fn provider_error_downcast() {
let err = anyhow::Error::from(ProviderError::new(400, "test", "bad request body", None));
let downcasted = err.downcast_ref::<ProviderError>();
assert!(downcasted.is_some());
assert_eq!(downcasted.unwrap().status, 400);
let plain_err = anyhow::anyhow!("some other error");
assert!(plain_err.downcast_ref::<ProviderError>().is_none());
}
#[test]
fn provider_error_retry_after_extraction() {
let err = ProviderError::new(429, "test", "rate limited", Some(5000));
assert_eq!(err.retry_after_ms, Some(5000));
let err = ProviderError::new(200, "test", "ok", None);
assert_eq!(err.retry_after_ms, None);
}
#[test]
fn parse_retry_after_value_various_inputs() {
assert_eq!(parse_retry_after_value("5"), Some(5000));
assert_eq!(parse_retry_after_value("2.5"), Some(2500));
assert_eq!(parse_retry_after_value("0"), Some(0));
assert_eq!(parse_retry_after_value("10"), Some(10_000));
assert_eq!(parse_retry_after_value("120"), Some(120_000));
assert_eq!(parse_retry_after_value("-1"), None);
assert_eq!(parse_retry_after_value("abc"), None);
assert_eq!(parse_retry_after_value(""), None);
assert_eq!(parse_retry_after_value(" 5 "), Some(5000));
}
}