refactor(api): enhance error handling using error_stack crate
Some checks failed
build / checks-matrix (push) Successful in 19m20s
build / checks-build (push) Has been cancelled
docs / docs (push) Has been cancelled
build / codecov (push) Has been cancelled

This commit is contained in:
uttarayan21
2025-10-08 16:25:56 +05:30
parent 8139fe4cb3
commit e9ecd2a295
2 changed files with 87 additions and 53 deletions

View File

@@ -1,19 +1,8 @@
use crate::errors::{Error, Result};
use error_stack::{Report, ResultExt};
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ApiError {
#[error("HTTP request failed: {0}")]
Request(#[from] reqwest::Error),
#[error("Deserialization error: {0}")]
Deserialization(#[from] serde_path_to_error::Error<serde_json::Error>),
#[error("API error: {message}")]
Api { message: String },
}
pub type Result<T> = std::result::Result<T, ApiError>;
#[derive(Clone)] #[derive(Clone)]
pub struct SonarrClient { pub struct SonarrClient {
@@ -31,86 +20,125 @@ impl SonarrClient {
} }
} }
async fn get<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> Result<T> { async fn get<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> Result<T, Error> {
let url = format!("{}/api/v3{}", self.base_url, endpoint); let url = format!("{}/api/v3{}", self.base_url, endpoint);
let response = self let response = self
.client .client
.get(&url) .get(&url)
.header("X-Api-Key", &self.api_key) .header("X-Api-Key", &self.api_key)
.send() .send()
.await?; .await
.change_context(Error::Request)
.attach_printable("Failed to send HTTP request")?;
if !response.status().is_success() { if !response.status().is_success() {
return Err(ApiError::Api { let status = response.status();
message: format!("HTTP {}: {}", response.status(), response.text().await?), let error_text = response
}); .text()
.await
.change_context(Error::Request)
.attach_printable("Failed to read error response body")?;
return Err(Report::new(Error::Api)
.attach_printable(format!("HTTP {}: {}", status, error_text)));
} }
let text = response.text().await?; let text = response
.text()
.await
.change_context(Error::Request)
.attach_printable("Failed to read response body")?;
let deser = &mut serde_json::Deserializer::from_str(&text); let deser = &mut serde_json::Deserializer::from_str(&text);
serde_path_to_error::deserialize(deser).map_err(ApiError::from) serde_path_to_error::deserialize(deser)
.change_context(Error::Serialization)
.attach_printable("Failed to deserialize API response")
} }
#[allow(dead_code)] #[allow(dead_code)]
async fn get_debug<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> Result<T> { async fn get_debug<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> Result<T, Error> {
let url = format!("{}/api/v3{}", self.base_url, endpoint); let url = format!("{}/api/v3{}", self.base_url, endpoint);
let response = self let response = self
.client .client
.get(&url) .get(&url)
.header("X-Api-Key", &self.api_key) .header("X-Api-Key", &self.api_key)
.send() .send()
.await?; .await
.change_context(Error::Request)
.attach_printable("Failed to send debug HTTP request")?;
if !response.status().is_success() { if !response.status().is_success() {
return Err(ApiError::Api { let status = response.status();
message: format!("HTTP {}: {}", response.status(), response.text().await?), let error_text = response
}); .text()
.await
.change_context(Error::Request)
.attach_printable("Failed to read debug error response body")?;
return Err(Report::new(Error::Api)
.attach_printable(format!("Debug HTTP {}: {}", status, error_text)));
} }
let text = response.text().await?; let text = response
.text()
.await
.change_context(Error::Request)
.attach_printable("Failed to read debug response body")?;
let _ = std::fs::write(endpoint.replace("/", "_"), &text); let _ = std::fs::write(endpoint.replace("/", "_"), &text);
let deser = &mut serde_json::Deserializer::from_str(&text); let deser = &mut serde_json::Deserializer::from_str(&text);
serde_path_to_error::deserialize(deser).map_err(ApiError::from) serde_path_to_error::deserialize(deser)
.change_context(Error::Serialization)
.attach_printable("Failed to deserialize debug API response")
} }
#[allow(dead_code)] #[allow(dead_code)]
async fn post<T: Serialize, R: for<'de> Deserialize<'de>>( async fn post<T: Serialize, R: for<'de> Deserialize<'de>>(
&self, &self,
endpoint: &str, endpoint: &str,
data: &T, body: &T,
) -> Result<R> { ) -> Result<R, Error> {
let url = format!("{}/api/v3{}", self.base_url, endpoint); let url = format!("{}/api/v3{}", self.base_url, endpoint);
let response = self let response = self
.client .client
.post(&url) .post(&url)
.header("X-Api-Key", &self.api_key) .header("X-Api-Key", &self.api_key)
.json(data) .json(body)
.send() .send()
.await?; .await
.change_context(Error::Request)
.attach_printable("Failed to send POST request")?;
if !response.status().is_success() { if !response.status().is_success() {
return Err(ApiError::Api { let status = response.status();
message: format!("HTTP {}: {}", response.status(), response.text().await?), let error_text = response
}); .text()
.await
.change_context(Error::Request)
.attach_printable("Failed to read POST error response body")?;
return Err(Report::new(Error::Api)
.attach_printable(format!("POST HTTP {}: {}", status, error_text)));
} }
let text = response.text().await?; let text = response
.text()
.await
.change_context(Error::Request)
.attach_printable("Failed to read POST response body")?;
let deser = &mut serde_json::Deserializer::from_str(&text); let deser = &mut serde_json::Deserializer::from_str(&text);
serde_path_to_error::deserialize(deser).map_err(ApiError::from) serde_path_to_error::deserialize(deser)
.change_context(Error::Serialization)
.attach_printable("Failed to deserialize POST API response")
} }
pub async fn get_system_status(&self) -> Result<SystemStatus> { pub async fn get_system_status(&self) -> Result<SystemStatus, Error> {
self.get("/system/status").await self.get("/system/status").await
} }
pub async fn get_series(&self) -> Result<Vec<Series>> { pub async fn get_series(&self) -> Result<Vec<Series>, Error> {
self.get("/series").await self.get("/series").await
} }
#[allow(dead_code)] #[allow(dead_code)]
pub async fn get_series_by_id(&self, id: u32) -> Result<Series> { pub async fn get_series_by_id(&self, id: u32) -> Result<Series, Error> {
self.get(&format!("/series/{}", id)).await self.get(&format!("/series/{}", id)).await
} }
@@ -119,7 +147,7 @@ impl SonarrClient {
&self, &self,
series_id: Option<u32>, series_id: Option<u32>,
season_number: Option<u32>, season_number: Option<u32>,
) -> Result<Vec<Episode>> { ) -> Result<Vec<Episode>, Error> {
let mut query = Vec::new(); let mut query = Vec::new();
if let Some(id) = series_id { if let Some(id) = series_id {
query.push(format!("seriesId={}", id)); query.push(format!("seriesId={}", id));
@@ -141,7 +169,7 @@ impl SonarrClient {
&self, &self,
start: Option<&str>, start: Option<&str>,
end: Option<&str>, end: Option<&str>,
) -> Result<Vec<Episode>> { ) -> Result<Vec<Episode>, Error> {
let mut query = Vec::new(); let mut query = Vec::new();
if let Some(start_date) = start { if let Some(start_date) = start {
query.push(format!("start={}", start_date)); query.push(format!("start={}", start_date));
@@ -159,24 +187,24 @@ impl SonarrClient {
self.get(&format!("/calendar{}", query_string)).await self.get(&format!("/calendar{}", query_string)).await
} }
pub async fn get_queue(&self) -> Result<QueuePagingResource> { pub async fn get_queue(&self) -> Result<QueuePagingResource, Error> {
self.get("/queue").await self.get("/queue").await
} }
pub async fn get_history(&self) -> Result<HistoryPagingResource> { pub async fn get_history(&self) -> Result<HistoryPagingResource, Error> {
self.get("/history").await self.get("/history").await
} }
#[allow(dead_code)] #[allow(dead_code)]
pub async fn get_missing_episodes(&self) -> Result<EpisodePagingResource> { pub async fn get_missing_episodes(&self) -> Result<EpisodePagingResource, Error> {
self.get("/wanted/missing").await self.get("/wanted/missing").await
} }
pub async fn get_health(&self) -> Result<Vec<HealthResource>> { pub async fn get_health(&self) -> Result<Vec<HealthResource>, Error> {
self.get("/health").await self.get("/health").await
} }
pub async fn search_series(&self, term: &str) -> Result<Vec<Series>> { pub async fn search_series(&self, term: &str) -> Result<Vec<Series>, Error> {
self.get(&format!( self.get(&format!(
"/series/lookup?term={}", "/series/lookup?term={}",
urlencoding::encode(term) urlencoding::encode(term)
@@ -185,7 +213,7 @@ impl SonarrClient {
} }
#[allow(dead_code)] #[allow(dead_code)]
pub async fn add_series(&self, series: &Series) -> Result<Series> { pub async fn add_series(&self, series: &Series) -> Result<Series, Error> {
self.post("/series", series).await self.post("/series", series).await
} }
} }

View File

@@ -1,7 +1,13 @@
// Removed unused imports Report and ResultExt use error_stack::Report;
#[derive(Debug, thiserror::Error)]
#[error("An error occurred")]
pub struct Error;
#[allow(dead_code)] #[derive(Debug, thiserror::Error)]
pub type Result<T, E = error_stack::Report<Error>> = core::result::Result<T, E>; pub enum Error {
#[error("API error")]
Api,
#[error("HTTP request failed")]
Request,
#[error("Serialization/Deserialization error")]
Serialization,
}
pub type Result<T, E = Error> = core::result::Result<T, Report<E>>;