//! HTTP error mapping. A blocked verdict is NOT an error and never lands
//! here; these are the genuinely failed requests.
use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use jac_decision::DecisionError;
use jac_object::ObjectError;
use jac_repo::{RefError, RepoError};
use serde::Serialize;
use thiserror::Error;
/// A failed request, carrying the status and a stable machine code.
#[derive(Debug, Error)]
pub(crate) enum ApiError {
/// Something the request named does not exist.
#[error("{0}")]
NotFound(String),
/// The request was well-formed JSON but semantically invalid.
#[error("{0}")]
Invalid(String),
/// The request conflicts with existing state.
#[error("{0}")]
Conflict(String),
/// Promotion refused: fast-forward-only in this milestone.
#[error("promotion is fast-forward-only and `{from}` does not descend from `{into}`")]
NonFastForward {
/// Source ref.
from: String,
/// Target ref.
into: String,
},
/// A capability this surface can offer but is not configured for. Not a
/// failure: the caller is expected to fall back.
#[error("{0}")]
Unavailable(String),
/// The server itself failed.
#[error("{0}")]
Internal(String),
}
impl ApiError {
pub(crate) fn not_found(what: impl Into<String>) -> Self {
Self::NotFound(what.into())
}
pub(crate) fn invalid(why: impl Into<String>) -> Self {
Self::Invalid(why.into())
}
pub(crate) fn internal(why: impl Into<String>) -> Self {
Self::Internal(why.into())
}
const fn status(&self) -> StatusCode {
match self {
Self::NotFound(_) => StatusCode::NOT_FOUND,
Self::Invalid(_) => StatusCode::UNPROCESSABLE_ENTITY,
Self::Conflict(_) | Self::NonFastForward { .. } => StatusCode::CONFLICT,
Self::Unavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
const fn code(&self) -> &'static str {
match self {
Self::NotFound(_) => "not-found",
Self::Invalid(_) => "invalid",
Self::Conflict(_) => "conflict",
Self::NonFastForward { .. } => "non-fast-forward",
Self::Unavailable(_) => "unavailable",
Self::Internal(_) => "internal",
}
}
}
#[derive(Debug, Serialize)]
struct ErrorBody {
error: ErrorDetail,
}
#[derive(Debug, Serialize)]
struct ErrorDetail {
code: &'static str,
message: String,
}
/// Carries a server-fault message from the handler layer to the telemetry
/// middleware via response extensions, so a 500 is reported as a bug
/// without every handler touching the telemetry handle directly.
#[derive(Debug, Clone)]
pub(crate) struct ErrorSignal(pub String);
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let is_internal = matches!(self, Self::Internal(_));
let message = self.to_string();
let body = ErrorBody {
error: ErrorDetail {
code: self.code(),
message: message.clone(),
},
};
let mut response = (self.status(), Json(body)).into_response();
if is_internal {
response.extensions_mut().insert(ErrorSignal(message));
}
response
}
}
impl From<RepoError> for ApiError {
fn from(e: RepoError) -> Self {
match e {
RepoError::Store(store) => store.into(),
RepoError::Tree(_) | RepoError::Path(_) => Self::Invalid(e.to_string()),
RepoError::RefNotFound(_) | RepoError::NoSuchPendingDecision => {
Self::NotFound(e.to_string())
}
RepoError::Decision(d) => d.into(),
RepoError::NonFastForward { from, into } => Self::NonFastForward { from, into },
// `RepoError` is non-exhaustive; an error this surface does not
// recognise is a server fault, not a client one.
_ => Self::Internal(e.to_string()),
}
}
}
impl From<ObjectError> for ApiError {
fn from(e: ObjectError) -> Self {
match e {
ObjectError::BlobNotFound
| ObjectError::TreeNotFound
| ObjectError::SnapshotNotFound => Self::NotFound(e.to_string()),
// `Corrupt` and any future variant are server faults alike.
_ => Self::Internal(e.to_string()),
}
}
}
impl From<DecisionError> for ApiError {
fn from(e: DecisionError) -> Self {
match e {
// Surface the exact validation message; the frontend mirrors it.
DecisionError::InvalidStatement => Self::Invalid(e.to_string()),
// `WrongDecision` cannot arise from a request; server fault.
_ => Self::Internal(e.to_string()),
}
}
}
impl From<RefError> for ApiError {
fn from(e: RefError) -> Self {
Self::Invalid(e.to_string())
}
}