pinakes/crates/pinakes-server/src/error.rs
NotAShelf 625077f341
pinakes-server: add utoipa annotations to all routes; fix tests
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I28cf5b7b7cff8e90e123d624d97cf9656a6a6964
2026-03-22 17:58:39 +03:00

133 lines
4.1 KiB
Rust

use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Serialize;
#[derive(Debug, Serialize)]
struct ErrorResponse {
error: String,
}
pub struct ApiError(pub pinakes_core::error::PinakesError);
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
use pinakes_core::error::PinakesError;
let (status, message) = match &self.0 {
PinakesError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
PinakesError::FileNotFound(path) => {
// Only expose the file name, not the full path
let name = path.file_name().map_or_else(
|| "unknown".to_string(),
|n| n.to_string_lossy().to_string(),
);
tracing::debug!(path = %path.display(), "file not found");
(StatusCode::NOT_FOUND, format!("file not found: {name}"))
},
PinakesError::TagNotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
PinakesError::CollectionNotFound(msg) => {
(StatusCode::NOT_FOUND, msg.clone())
},
PinakesError::DuplicateHash(msg) => (StatusCode::CONFLICT, msg.clone()),
PinakesError::UnsupportedMediaType(path) => {
let name = path.file_name().map_or_else(
|| "unknown".to_string(),
|n| n.to_string_lossy().to_string(),
);
(
StatusCode::BAD_REQUEST,
format!("unsupported media type: {name}"),
)
},
PinakesError::SearchParse(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
PinakesError::InvalidOperation(msg) => {
(StatusCode::BAD_REQUEST, msg.clone())
},
PinakesError::InvalidLanguageCode(code) => {
(
StatusCode::BAD_REQUEST,
format!("invalid language code: {code}"),
)
},
PinakesError::SubtitleTrackNotFound { index } => {
(
StatusCode::NOT_FOUND,
format!("subtitle track {index} not found in media"),
)
},
PinakesError::ExternalTool { tool, .. } => {
tracing::error!(tool = %tool, error = %self.0, "external tool failed");
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("external tool `{tool}` failed"),
)
},
PinakesError::Authentication(msg) => {
(StatusCode::UNAUTHORIZED, msg.clone())
},
PinakesError::Authorization(msg) => (StatusCode::FORBIDDEN, msg.clone()),
PinakesError::Serialization(msg) => {
tracing::error!(error = %msg, "serialization error");
(
StatusCode::INTERNAL_SERVER_ERROR,
"data serialization error".to_string(),
)
},
PinakesError::Config(_) => {
tracing::error!(error = %self.0, "configuration error");
(
StatusCode::INTERNAL_SERVER_ERROR,
"internal configuration error".to_string(),
)
},
_ => {
tracing::error!(error = %self.0, "internal server error");
(
StatusCode::INTERNAL_SERVER_ERROR,
"internal server error".to_string(),
)
},
};
let body = serde_json::to_string(&ErrorResponse {
error: message.clone(),
})
.unwrap_or_else(|_| format!(r#"{{"error":"{message}"}}"#));
(status, [("content-type", "application/json")], body).into_response()
}
}
impl From<pinakes_core::error::PinakesError> for ApiError {
fn from(e: pinakes_core::error::PinakesError) -> Self {
Self(e)
}
}
impl ApiError {
pub fn bad_request(msg: impl Into<String>) -> Self {
Self(pinakes_core::error::PinakesError::InvalidOperation(
msg.into(),
))
}
pub fn not_found(msg: impl Into<String>) -> Self {
Self(pinakes_core::error::PinakesError::NotFound(msg.into()))
}
pub fn internal(msg: impl Into<String>) -> Self {
Self(pinakes_core::error::PinakesError::Database(msg.into()))
}
pub fn forbidden(msg: impl Into<String>) -> Self {
Self(pinakes_core::error::PinakesError::Authorization(msg.into()))
}
pub fn unauthorized(msg: impl Into<String>) -> Self {
Self(pinakes_core::error::PinakesError::Authentication(
msg.into(),
))
}
}
pub type ApiResult<T> = Result<T, ApiError>;