use std::time::Duration;
use progenitor_client::Error as ProgenitorError;
use reqwest::header::HeaderMap;
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum TmdbError {
#[error("HTTP transport error: {0}")]
Http(#[from] reqwest::Error),
#[error("TMDB API error (HTTP {status}): {message}")]
Api {
status: u16,
status_code: Option<i64>,
message: String,
retry_after: Option<Duration>,
},
#[error("deserialization error: {0}")]
Deserialization(#[from] serde_json::Error),
#[error("invalid configuration: {0}")]
InvalidConfig(String),
}
#[derive(Debug, serde::Deserialize)]
struct TmdbErrorBody {
status_code: Option<i64>,
status_message: Option<String>,
}
impl TmdbError {
pub(crate) fn is_retryable(&self) -> bool {
match self {
TmdbError::Api { status, .. } => *status == 429 || (500..=599).contains(status),
TmdbError::Http(e) => e.is_timeout() || e.is_connect(),
_ => false,
}
}
pub(crate) fn retry_after(&self) -> Option<Duration> {
match self {
TmdbError::Api { retry_after, .. } => *retry_after,
_ => None,
}
}
}
impl TmdbError {
pub(crate) async fn from_progenitor<E>(err: ProgenitorError<E>) -> Self
where
E: std::fmt::Debug + Send + Sync + 'static,
{
match err {
ProgenitorError::CommunicationError(e)
| ProgenitorError::ResponseBodyError(e)
| ProgenitorError::InvalidUpgrade(e) => TmdbError::Http(e),
ProgenitorError::InvalidResponsePayload(_bytes, e) => TmdbError::Deserialization(e),
ProgenitorError::InvalidRequest(msg) => TmdbError::Api {
status: 0,
status_code: None,
message: format!("invalid request: {msg}"),
retry_after: None,
},
ProgenitorError::Custom(msg) => TmdbError::Api {
status: 0,
status_code: None,
message: format!("client hook error: {msg}"),
retry_after: None,
},
ProgenitorError::ErrorResponse(rv) => {
let status = rv.status().as_u16();
let retry_after = parse_retry_after(rv.headers());
TmdbError::Api {
status,
status_code: None,
message: format!("{:?}", rv.into_inner()),
retry_after,
}
}
ProgenitorError::UnexpectedResponse(resp) => {
let status = resp.status().as_u16();
let retry_after = parse_retry_after(resp.headers());
let body = resp.text().await.unwrap_or_default();
let (status_code, message) = decode_tmdb_error_body(&body, status);
TmdbError::Api {
status,
status_code,
message,
retry_after,
}
}
}
}
}
impl<E: std::fmt::Debug + Send + Sync + 'static> From<ProgenitorError<E>> for TmdbError {
fn from(err: ProgenitorError<E>) -> Self {
match err {
ProgenitorError::CommunicationError(e)
| ProgenitorError::ResponseBodyError(e)
| ProgenitorError::InvalidUpgrade(e) => TmdbError::Http(e),
ProgenitorError::InvalidResponsePayload(_, e) => TmdbError::Deserialization(e),
ProgenitorError::InvalidRequest(msg) => TmdbError::Api {
status: 0,
status_code: None,
message: format!("invalid request: {msg}"),
retry_after: None,
},
ProgenitorError::Custom(msg) => TmdbError::Api {
status: 0,
status_code: None,
message: format!("client hook error: {msg}"),
retry_after: None,
},
ProgenitorError::ErrorResponse(ref rv) => {
let status = rv.status().as_u16();
let retry_after = parse_retry_after(rv.headers());
TmdbError::Api {
status,
status_code: None,
message: format!("{err}"),
retry_after,
}
}
ProgenitorError::UnexpectedResponse(ref resp) => {
let status = resp.status().as_u16();
let retry_after = parse_retry_after(resp.headers());
TmdbError::Api {
status,
status_code: None,
message: format!("{err}"),
retry_after,
}
}
}
}
}
fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
let raw = headers.get(reqwest::header::RETRY_AFTER)?.to_str().ok()?;
raw.trim().parse::<u64>().ok().map(Duration::from_secs)
}
fn decode_tmdb_error_body(body: &str, status: u16) -> (Option<i64>, String) {
match serde_json::from_str::<TmdbErrorBody>(body) {
Ok(parsed) => {
let message = parsed
.status_message
.filter(|m| !m.is_empty())
.unwrap_or_else(|| format!("HTTP {status}"));
(parsed.status_code, message)
}
Err(_) => {
let trimmed = body.trim();
let message = if trimmed.is_empty() {
format!("HTTP {status}")
} else {
trimmed.to_string()
};
(None, message)
}
}
}