Files
arr/crates/arr-compat/src/error.rs
T
naps62-yolo 94c334ffe9
ci / web (push) Successful in 12s
ci / rust (push) Successful in 1m44s
e2e / e2e (push) Successful in 1m21s
Radarr v3 shim for Jellyseerr (#62)
2026-08-22 21:00:35 +01:00

101 lines
3.2 KiB
Rust

//! Failures, in the shapes Radarr produces them.
//!
//! Jellyseerr surfaces the response body verbatim in its own logs and settings
//! UI, so a validation failure that comes back in Radarr's shape is readable
//! there without anyone knowing this is not Radarr.
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Serialize;
/// Everything the shim can answer with instead of a result.
#[derive(Debug)]
pub enum CompatError {
/// The request was well-formed but names something that does not exist
/// here — usually a root folder path Jellyseerr was configured with by
/// hand. `property` is Radarr's field name, `PascalCase` as it sends it.
Validation {
/// Radarr's name for the offending field, e.g. `RootFolderPath`.
property: &'static str,
/// What is wrong with it, in a sentence.
message: String,
},
/// No such movie.
NotFound,
/// TMDB is unconfigured or would not answer.
Upstream(String),
/// The database would not answer.
Database(String),
}
/// One entry of Radarr's validation-failure body.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ValidationFailure {
property_name: &'static str,
error_message: String,
severity: &'static str,
}
/// Radarr's shape for everything that is not a validation failure.
#[derive(Debug, Serialize)]
struct Message {
message: String,
}
impl IntoResponse for CompatError {
fn into_response(self) -> Response {
match self {
// A list, not an object: Radarr's model binder reports every
// failing field at once and Jellyseerr's client reads index 0.
Self::Validation { property, message } => (
StatusCode::BAD_REQUEST,
Json(vec![ValidationFailure {
property_name: property,
error_message: message,
severity: "error",
}]),
)
.into_response(),
Self::NotFound => (
StatusCode::NOT_FOUND,
Json(Message {
message: "NotFound".into(),
}),
)
.into_response(),
Self::Upstream(detail) => {
tracing::warn!(%detail, "compat shim upstream failure");
(StatusCode::BAD_GATEWAY, Json(Message { message: detail })).into_response()
}
Self::Database(detail) => {
tracing::error!(%detail, "compat shim database error");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(Message {
message: "database error".into(),
}),
)
.into_response()
}
}
}
}
impl From<sqlx::Error> for CompatError {
fn from(error: sqlx::Error) -> Self {
if matches!(error, sqlx::Error::RowNotFound) {
Self::NotFound
} else {
Self::Database(error.to_string())
}
}
}
impl From<arr_meta::Error> for CompatError {
fn from(error: arr_meta::Error) -> Self {
Self::Upstream(error.to_string())
}
}