Skip to main content

unitycatalog_server/
error.rs

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