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 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 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::MissingRecipient => {
287 Status::invalid_argument("Failed to extract recipient from request")
288 }
289 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::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}