Skip to main content

unitycatalog_delta_api/
error.rs

1//! The `/delta/v1` error contract, decoupled from any server's internal error.
2//!
3//! Two error types live here:
4//!
5//! - [`DeltaBackendError`] — what the backend [port](crate::backend::DeltaBackend)
6//!   returns. It is a *pre-image* of the Delta error envelope: each variant maps
7//!   to a fixed `(StatusCode, DeltaErrorType)`. Each server's adapter converts its
8//!   own internal error into this enum, so the crate never sees a server error
9//!   type.
10//! - [`DeltaApiError`] — the error half of every handler `Result`. It wraps a
11//!   [`DeltaBackendError`] plus the crate's own logic errors (contract validation,
12//!   commit arbitration), and its [`IntoResponse`] emits the Delta envelope
13//!   (`{ "error": { message, type, code } }`) with the exact status codes the
14//!   reference `DeltaApiExceptionHandler` uses.
15//!
16//! [`DeltaApiError`]'s [`IntoResponse`] implementation reproduces the previous
17//! server-side `DeltaError` variant → status mapping exactly, so response bodies
18//! and status codes are unchanged by the extraction.
19
20use axum::Json;
21use axum::http::StatusCode;
22use axum::response::{IntoResponse, Response};
23
24use crate::models::{DeltaErrorModel, DeltaErrorResponse, DeltaErrorType};
25
26/// Error returned by a [`DeltaBackend`](crate::backend::DeltaBackend) operation.
27///
28/// Each variant has a fixed `(StatusCode, DeltaErrorType)` target, applied by
29/// [`DeltaApiError`]'s [`IntoResponse`]. A server adapter converts its internal
30/// error into one of these variants, preserving the response semantics without
31/// exposing its own error type to the crate.
32#[derive(Debug, thiserror::Error)]
33pub enum DeltaBackendError {
34    /// The requested resource does not exist. → 404 `NoSuchTableException`.
35    #[error("{0}")]
36    NotFound(String),
37
38    /// A backend failure with no recognized semantics, surfaced as a generic
39    /// not-found. → 404 `NotFoundException`.
40    #[error("{0}")]
41    NotFoundGeneric(String),
42
43    /// A resource with the same identity already exists. → 409 `AlreadyExistsException`.
44    #[error("{0}")]
45    AlreadyExists(String),
46
47    /// The caller is authenticated but not permitted. → 403 `PermissionDeniedException`.
48    #[error("{0}")]
49    PermissionDenied(String),
50
51    /// The caller is not authenticated. → 401 `NotAuthorizedException`.
52    #[error("{0}")]
53    Unauthenticated(String),
54
55    /// The request is malformed or a parameter is invalid. → 400 `InvalidParameterValueException`.
56    #[error("invalid argument: {0}")]
57    InvalidArgument(String),
58
59    /// A commit lost the version race / was already accepted. → 409 `CommitVersionConflictException`.
60    #[error("commit version conflict: {0}")]
61    CommitVersionConflict(String),
62
63    /// An `assert-etag` / `assert-table-uuid` requirement was not met. → 409 `UpdateRequirementConflictException`.
64    #[error("update requirement conflict: {0}")]
65    UpdateRequirementConflict(String),
66
67    /// The request was throttled or hit a resource limit. → 429 `ResourceExhaustedException`.
68    #[error("resource exhausted: {0}")]
69    ResourceExhausted(String),
70
71    /// The requested functionality is not implemented. → 501 `NotImplementedException`.
72    #[error("not implemented: {0}")]
73    NotImplemented(&'static str),
74
75    /// An unexpected backend failure. → 500 `InternalServerErrorException`.
76    #[error("internal error: {0}")]
77    Internal(String),
78}
79
80/// The error half of every Delta handler `Result`.
81///
82/// Wraps a [`DeltaBackendError`]; the crate's own logic errors are constructed via
83/// the helper constructors ([`invalid_argument`](Self::invalid_argument), etc.).
84/// Its [`IntoResponse`] serializes the Delta API error envelope.
85#[derive(Debug, thiserror::Error)]
86#[error(transparent)]
87pub struct DeltaApiError(#[from] pub DeltaBackendError);
88
89impl DeltaApiError {
90    /// A 400 `InvalidParameterValueException` from crate logic (e.g. contract validation).
91    pub fn invalid_argument(message: impl Into<String>) -> Self {
92        DeltaApiError(DeltaBackendError::InvalidArgument(message.into()))
93    }
94
95    /// A 403 `PermissionDeniedException`.
96    pub fn permission_denied(message: impl Into<String>) -> Self {
97        DeltaApiError(DeltaBackendError::PermissionDenied(message.into()))
98    }
99
100    /// A 404 `NoSuchTableException`.
101    pub fn not_found(message: impl Into<String>) -> Self {
102        DeltaApiError(DeltaBackendError::NotFound(message.into()))
103    }
104
105    /// A 501 `NotImplementedException`.
106    pub fn not_implemented(what: &'static str) -> Self {
107        DeltaApiError(DeltaBackendError::NotImplemented(what))
108    }
109
110    /// The `(status, error-type)` pair for the wrapped error. Reproduces the
111    /// previous server-side `DeltaError::parts` mapping exactly.
112    fn parts(&self) -> (StatusCode, DeltaErrorType) {
113        use DeltaErrorType::*;
114        match &self.0 {
115            DeltaBackendError::NotFound(_) => (StatusCode::NOT_FOUND, NoSuchTableException),
116            DeltaBackendError::NotFoundGeneric(_) => (StatusCode::NOT_FOUND, NotFoundException),
117            DeltaBackendError::AlreadyExists(_) => (StatusCode::CONFLICT, AlreadyExistsException),
118            DeltaBackendError::PermissionDenied(_) => {
119                (StatusCode::FORBIDDEN, PermissionDeniedException)
120            }
121            DeltaBackendError::Unauthenticated(_) => {
122                (StatusCode::UNAUTHORIZED, NotAuthorizedException)
123            }
124            DeltaBackendError::InvalidArgument(_) => {
125                (StatusCode::BAD_REQUEST, InvalidParameterValueException)
126            }
127            DeltaBackendError::CommitVersionConflict(_) => {
128                (StatusCode::CONFLICT, CommitVersionConflictException)
129            }
130            DeltaBackendError::UpdateRequirementConflict(_) => {
131                (StatusCode::CONFLICT, UpdateRequirementConflictException)
132            }
133            DeltaBackendError::ResourceExhausted(_) => {
134                (StatusCode::TOO_MANY_REQUESTS, ResourceExhaustedException)
135            }
136            DeltaBackendError::NotImplemented(_) => {
137                (StatusCode::NOT_IMPLEMENTED, NotImplementedException)
138            }
139            DeltaBackendError::Internal(_) => (
140                StatusCode::INTERNAL_SERVER_ERROR,
141                InternalServerErrorException,
142            ),
143        }
144    }
145}
146
147impl IntoResponse for DeltaApiError {
148    fn into_response(self) -> Response {
149        let (status, error_type) = self.parts();
150        let body = DeltaErrorResponse {
151            error: DeltaErrorModel {
152                message: self.0.to_string(),
153                error_type,
154                code: status.as_u16(),
155                stack: None,
156            },
157        };
158        (status, Json(body)).into_response()
159    }
160}
161
162/// Result type used across the crate's Delta logic and handler surface.
163pub type DeltaApiResult<T> = Result<T, DeltaApiError>;