jacquardSnapshot

← snapshot

5037 bytes
//! 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())
    }
}