use axum::http::StatusCode; use axum::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(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| "unknown".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(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| "unknown".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::Authentication(msg) => (StatusCode::UNAUTHORIZED, msg.clone()), PinakesError::Authorization(msg) => (StatusCode::FORBIDDEN, msg.clone()), 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 for ApiError { fn from(e: pinakes_core::error::PinakesError) -> Self { Self(e) } } impl ApiError { pub fn bad_request(msg: impl Into) -> Self { Self(pinakes_core::error::PinakesError::InvalidOperation( msg.into(), )) } pub fn not_found(msg: impl Into) -> Self { Self(pinakes_core::error::PinakesError::NotFound(msg.into())) } pub fn internal(msg: impl Into) -> Self { Self(pinakes_core::error::PinakesError::Database(msg.into())) } pub fn forbidden(msg: impl Into) -> Self { Self(pinakes_core::error::PinakesError::Authorization(msg.into())) } pub fn unauthorized(msg: impl Into) -> Self { Self(pinakes_core::error::PinakesError::Authentication( msg.into(), )) } } pub type ApiResult = Result;