Skip to main content

unitycatalog_server/
error.rs

1use axum::extract::Json;
2use axum::http::StatusCode;
3use axum::response::{IntoResponse, Response};
4#[cfg(feature = "grpc")]
5use tonic::Status;
6use tracing::error;
7use unitycatalog_common::ErrorResponse;
8
9pub type Result<T, E = Error> = std::result::Result<T, E>;
10
11#[derive(Debug, thiserror::Error)]
12pub enum Error {
13    #[error("Common Error: {source}")]
14    Common {
15        #[from]
16        source: unitycatalog_common::Error,
17    },
18
19    #[error("Object Store Error: {source}")]
20    ObjectStore {
21        #[from]
22        source: object_store::Error,
23    },
24
25    #[error("Serialization Error: {source}")]
26    SerDe {
27        #[from]
28        source: serde_json::Error,
29    },
30
31    #[error("Entity not found.")]
32    NotFound,
33
34    #[error("No or invalid token provided.")]
35    Unauthenticated,
36
37    #[error("Recipient is not allowed to read the entity.")]
38    NotAllowed,
39
40    #[error("Already exists")]
41    AlreadyExists,
42
43    #[error("Commit version conflict: {0}")]
44    CommitVersionConflict(String),
45
46    #[error("Update requirement conflict: {0}")]
47    UpdateRequirementConflict(String),
48
49    #[error("Resource exhausted: {0}")]
50    ResourceExhausted(String),
51
52    #[error("Invalid argument")]
53    InvalidArgument(String),
54
55    #[error("Invalid identifier: {0}")]
56    InvalidIdentifier(#[from] uuid::Error),
57
58    #[error("Missing recipient")]
59    MissingRecipient,
60
61    #[error("Generic error: {0}")]
62    Generic(String),
63
64    #[error("Not implemented: {0}")]
65    NotImplemented(&'static str),
66
67    #[error("Cloud credential error: {source}")]
68    CloudCredential {
69        #[from]
70        source: olai_http::Error,
71    },
72
73    #[error("Resource store error: {source}")]
74    ResourceStore {
75        #[from]
76        source: olai_store::Error,
77    },
78}
79
80impl Error {
81    pub fn generic(message: impl ToString) -> Self {
82        Error::Generic(message.to_string())
83    }
84
85    pub fn invalid_argument(message: impl ToString) -> Self {
86        Error::InvalidArgument(message.to_string())
87    }
88
89    /// Returns a machine-readable error code matching the UC API spec.
90    pub fn error_code(&self) -> &str {
91        match self {
92            Error::NotFound => "RESOURCE_NOT_FOUND",
93            Error::AlreadyExists => "RESOURCE_ALREADY_EXISTS",
94            Error::CommitVersionConflict(_) => "COMMIT_VERSION_CONFLICT",
95            Error::UpdateRequirementConflict(_) => "UPDATE_REQUIREMENT_CONFLICT",
96            Error::ResourceExhausted(_) => "RESOURCE_EXHAUSTED",
97            Error::NotAllowed => "PERMISSION_DENIED",
98            Error::Unauthenticated => "UNAUTHENTICATED",
99            Error::InvalidArgument(_) => "INVALID_PARAMETER_VALUE",
100            Error::InvalidIdentifier(_) => "INVALID_PARAMETER_VALUE",
101            Error::MissingRecipient => "INVALID_PARAMETER_VALUE",
102            Error::Common { source } => source.error_code(),
103            Error::ObjectStore { .. } => "INTERNAL_ERROR",
104            Error::SerDe { .. } => "INTERNAL_ERROR",
105            Error::Generic(_) => "INTERNAL_ERROR",
106            Error::NotImplemented(_) => "NOT_IMPLEMENTED",
107            Error::CloudCredential { .. } => "INTERNAL_ERROR",
108            Error::ResourceStore { source } => match source {
109                olai_store::Error::NotFound => "RESOURCE_NOT_FOUND",
110                olai_store::Error::AlreadyExists => "RESOURCE_ALREADY_EXISTS",
111                olai_store::Error::InvalidArgument(_) => "INVALID_PARAMETER_VALUE",
112                olai_store::Error::InvalidIdentifier(_) => "INVALID_PARAMETER_VALUE",
113                _ => "INTERNAL_ERROR",
114            },
115        }
116    }
117}
118
119const INTERNAL_ERROR: (StatusCode, &str) = (
120    StatusCode::INTERNAL_SERVER_ERROR,
121    "The request is not handled correctly due to a server error.",
122);
123
124const INVALID_ARGUMENT: (StatusCode, &str) = (
125    StatusCode::BAD_REQUEST,
126    "Invalid argument provided in the request.",
127);
128
129impl IntoResponse for Error {
130    fn into_response(self) -> Response {
131        let error_code = self.error_code().to_string();
132        let (status, message) = match self {
133            Error::Common { source } => {
134                let (status, message) = source.response_parts();
135                return (
136                    status,
137                    Json(ErrorResponse {
138                        error_code,
139                        message: message.to_string(),
140                    }),
141                )
142                    .into_response();
143            }
144            // EXTERNAL ERRORS
145            Error::ObjectStore { source } => match &source {
146                object_store::Error::NotFound { .. } => (
147                    StatusCode::NOT_FOUND,
148                    "The requested resource does not exist.",
149                ),
150                object_store::Error::AlreadyExists { .. } => {
151                    (StatusCode::CONFLICT, "The resource already exists.")
152                }
153                _ => {
154                    error!("Object store error: {}", source);
155                    INTERNAL_ERROR
156                }
157            },
158            Error::SerDe { source } => {
159                error!("Serialization error: {}", source);
160                INTERNAL_ERROR
161            }
162            Error::NotFound => (
163                StatusCode::NOT_FOUND,
164                "The requested resource does not exist.",
165            ),
166            Error::NotAllowed => (
167                StatusCode::FORBIDDEN,
168                "The request is forbidden from being fulfilled.",
169            ),
170            Error::AlreadyExists => (StatusCode::CONFLICT, "The resource already exists."),
171            Error::CommitVersionConflict(message) => {
172                error!("Commit version conflict: {}", message);
173                (
174                    StatusCode::CONFLICT,
175                    "The commit version was already accepted by another writer.",
176                )
177            }
178            Error::UpdateRequirementConflict(message) => {
179                error!("Update requirement conflict: {}", message);
180                (
181                    StatusCode::CONFLICT,
182                    "An update requirement (assert-table-uuid / assert-etag) was not met.",
183                )
184            }
185            Error::ResourceExhausted(message) => {
186                error!("Resource exhausted: {}", message);
187                (
188                    StatusCode::TOO_MANY_REQUESTS,
189                    "The maximum number of unbackfilled commits for this table was reached.",
190                )
191            }
192            Error::Unauthenticated => (
193                StatusCode::UNAUTHORIZED,
194                "The request is unauthenticated. The bearer token is missing or incorrect.",
195            ),
196            Error::InvalidArgument(message) => {
197                error!("Invalid argument: {}", message);
198                INVALID_ARGUMENT
199            }
200            Error::InvalidIdentifier(e) => {
201                error!("Invalid identifier: {}", e);
202                INVALID_ARGUMENT
203            }
204            Error::Generic(message) => {
205                error!("Generic error: {}", message);
206                INTERNAL_ERROR
207            }
208            Error::NotImplemented(what) => {
209                error!("Not implemented: {}", what);
210                (StatusCode::NOT_IMPLEMENTED, "Endpoint not implemented yet.")
211            }
212            Error::CloudCredential { source } => {
213                error!("Cloud credential error: {}", source);
214                INTERNAL_ERROR
215            }
216            Error::MissingRecipient => {
217                error!("Failed to extract recipient from request");
218                (
219                    StatusCode::BAD_REQUEST,
220                    "Failed to extract recipient from request",
221                )
222            }
223            Error::ResourceStore { source } => match source {
224                olai_store::Error::NotFound => (
225                    StatusCode::NOT_FOUND,
226                    "The requested resource does not exist.",
227                ),
228                olai_store::Error::AlreadyExists => {
229                    (StatusCode::CONFLICT, "The resource already exists.")
230                }
231                olai_store::Error::InvalidArgument(msg) => {
232                    error!("Invalid argument: {}", msg);
233                    INVALID_ARGUMENT
234                }
235                _ => {
236                    error!("Resource store error: {}", source);
237                    INTERNAL_ERROR
238                }
239            },
240        };
241
242        (
243            status,
244            Json(ErrorResponse {
245                error_code,
246                message: message.to_string(),
247            }),
248        )
249            .into_response()
250    }
251}
252
253#[cfg(feature = "grpc")]
254impl From<Error> for Status {
255    fn from(e: Error) -> Self {
256        match e {
257            Error::Common { source } => match source {
258                unitycatalog_common::Error::NotFound => {
259                    Status::not_found("The requested resource does not exist.")
260                }
261                unitycatalog_common::Error::InvalidArgument(msg) => Status::invalid_argument(msg),
262                unitycatalog_common::Error::InvalidIdentifier(e) => {
263                    Status::invalid_argument(e.to_string())
264                }
265                unitycatalog_common::Error::InvalidTableLocation(loc) => {
266                    Status::invalid_argument(format!("Invalid table location: {loc}"))
267                }
268                unitycatalog_common::Error::InvalidUrl(e) => {
269                    Status::invalid_argument(format!("Invalid URL: {e}"))
270                }
271                _ => Status::internal(format!("Common Error: {source}")),
272            },
273            Error::NotFound => Status::not_found("The requested resource does not exist."),
274            Error::NotAllowed => {
275                Status::permission_denied("The request is forbidden from being fulfilled.")
276            }
277            Error::Unauthenticated => Status::unauthenticated(
278                "The request is unauthenticated. The bearer token is missing or incorrect.",
279            ),
280            Error::SerDe { source } => {
281                Status::internal(format!("Serialization/Deserialization Error: {}", source))
282            }
283            // Error::InvalidTableLocation(location) => {
284            //     Status::internal(format!("Invalid table location: {}", location))
285            // }
286            Error::MissingRecipient => {
287                Status::invalid_argument("Failed to extract recipient from request")
288            }
289            // Error::DataFusion(error) => Status::internal(error.to_string()),
290            // Error::Arrow(error) => Status::internal(error.to_string()),
291            // Error::InvalidPredicate(msg) => Status::invalid_argument(msg),
292            Error::AlreadyExists => Status::already_exists("The resource already exists."),
293            Error::CommitVersionConflict(message) => Status::aborted(message),
294            Error::UpdateRequirementConflict(message) => Status::aborted(message),
295            Error::ResourceExhausted(message) => Status::resource_exhausted(message),
296            Error::InvalidIdentifier(e) => Status::invalid_argument(e.to_string()),
297            Error::InvalidArgument(message) => Status::invalid_argument(message),
298            Error::Generic(message) => Status::internal(message),
299            Error::NotImplemented(what) => {
300                Status::unimplemented(format!("Not implemented: {what}"))
301            }
302            // Error::Client(error) => Status::internal(error.to_string()),
303            // Error::InvalidUrl(_) => Status::internal("Invalid url"),
304            Error::ObjectStore { source } => {
305                Status::internal(format!("ObjectStore error: {}", source))
306            }
307            Error::CloudCredential { source } => {
308                Status::internal(format!("Cloud credential error: {}", source))
309            }
310            Error::ResourceStore { source } => match source {
311                olai_store::Error::NotFound => {
312                    Status::not_found("The requested resource does not exist.")
313                }
314                olai_store::Error::AlreadyExists => {
315                    Status::already_exists("The resource already exists.")
316                }
317                olai_store::Error::InvalidArgument(msg) => Status::invalid_argument(msg),
318                _ => Status::internal(format!("Resource store error: {source}")),
319            },
320        }
321    }
322}