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>;