1use unitycatalog_delta_api::models::DeltaErrorModel;
2
3pub type Result<T, E = Error> = std::result::Result<T, E>;
4
5#[derive(Debug, thiserror::Error)]
6pub enum Error {
7 #[error("Common Error: {source}")]
8 Common {
9 #[from]
10 source: unitycatalog_common::Error,
11 },
12
13 #[error("Delta API error {}: [{:?}] {}", .0.code, .0.error_type, .0.message)]
14 Delta(DeltaErrorModel),
15
16 #[cfg(not(target_arch = "wasm32"))]
19 #[error("Client Error: {source}")]
20 ClientError {
21 #[from]
22 source: olai_http::Error,
23 },
24
25 #[error("Malformed response: {source}")]
26 MalformedResponse {
27 #[from]
28 source: serde_json::Error,
29 },
30
31 #[error("Malformed url: {source}")]
32 MalformedUrl {
33 #[from]
34 source: url::ParseError,
35 },
36
37 #[error("Reqwuest error: {0}")]
38 RequestError(#[from] reqwest::Error),
39
40 #[error("API error: {0}")]
41 Api(#[from] UcApiError),
42
43 #[error("Generic error: {0}")]
44 Generic(String),
45}
46
47impl Error {
48 pub fn generic(message: impl ToString) -> Self {
49 Error::Generic(message.to_string())
50 }
51
52 #[cfg(not(target_arch = "wasm32"))]
61 pub(crate) fn from_delta_send(err: olai_http::SendRawError) -> Self {
62 match err {
63 olai_http::SendRawError::Sign(e) => Error::ClientError { source: e },
64 olai_http::SendRawError::Retry(e) => match e.status() {
65 Some(status) => parse_delta_error(status.as_u16(), e.body().map(str::as_bytes)),
66 None => Error::ClientError { source: e.error() },
68 },
69 }
70 }
71
72 #[cfg(not(target_arch = "wasm32"))]
76 pub(crate) fn from_api_send(err: olai_http::SendRawError) -> Self {
77 match err {
78 olai_http::SendRawError::Sign(e) => Error::ClientError { source: e },
79 olai_http::SendRawError::Retry(e) => match e.status() {
80 Some(status) => parse_error(status.as_u16(), e.body().map(str::as_bytes)),
81 None => Error::ClientError { source: e.error() },
82 },
83 }
84 }
85
86 #[cfg(target_arch = "wasm32")]
95 pub(crate) fn from_delta_send(err: reqwest::Error) -> Self {
96 Error::RequestError(err)
97 }
98
99 #[cfg(target_arch = "wasm32")]
101 pub(crate) fn from_api_send(err: reqwest::Error) -> Self {
102 Error::RequestError(err)
103 }
104
105 #[cfg(target_arch = "wasm32")]
111 pub(crate) fn delta_status(status: reqwest::StatusCode) -> Self {
112 parse_delta_error(status.as_u16(), None)
113 }
114}
115
116#[cfg(not(target_arch = "wasm32"))]
125pub(crate) async fn check_delta_response(response: reqwest::Response) -> Result<reqwest::Response> {
126 Ok(response)
127}
128
129#[cfg(target_arch = "wasm32")]
130pub(crate) async fn check_delta_response(response: reqwest::Response) -> Result<reqwest::Response> {
131 let status = response.status();
132 if status.is_success() {
133 return Ok(response);
134 }
135 let body = response.bytes().await.map_err(Error::RequestError)?;
136 Err(parse_delta_error(status.as_u16(), Some(&body)))
137}
138
139#[cfg(not(target_arch = "wasm32"))]
141pub(crate) async fn check_api_response(response: reqwest::Response) -> Result<reqwest::Response> {
142 Ok(response)
143}
144
145#[cfg(target_arch = "wasm32")]
146pub(crate) async fn check_api_response(response: reqwest::Response) -> Result<reqwest::Response> {
147 let status = response.status();
148 if status.is_success() {
149 return Ok(response);
150 }
151 let body = response.bytes().await.map_err(Error::RequestError)?;
152 Err(parse_error(status.as_u16(), Some(&body)))
153}
154
155impl Error {
156 pub fn is_not_found(&self) -> bool {
157 match self {
158 Error::Api(UcApiError::NotFound { .. }) => true,
159 Error::Delta(model) => model.error_type.is_not_found(),
160 _ => false,
161 }
162 }
163
164 pub fn is_already_exists(&self) -> bool {
165 match self {
166 Error::Api(UcApiError::AlreadyExists { .. }) => true,
167 Error::Delta(model) => model.error_type.is_already_exists(),
168 _ => false,
169 }
170 }
171
172 pub fn is_permission_denied(&self) -> bool {
173 match self {
174 Error::Api(UcApiError::PermissionDenied { .. }) => true,
175 Error::Delta(model) => {
176 matches!(
177 model.error_type,
178 unitycatalog_delta_api::models::DeltaErrorType::PermissionDeniedException
179 )
180 }
181 _ => false,
182 }
183 }
184
185 pub fn is_unauthenticated(&self) -> bool {
186 match self {
187 Error::Api(UcApiError::Unauthenticated { .. }) => true,
188 Error::Delta(model) => {
189 matches!(
190 model.error_type,
191 unitycatalog_delta_api::models::DeltaErrorType::NotAuthorizedException
192 )
193 }
194 _ => false,
195 }
196 }
197
198 pub fn is_commit_conflict(&self) -> bool {
202 matches!(self, Error::Delta(model) if model.error_type.is_commit_conflict())
203 }
204
205 pub fn is_update_requirement_conflict(&self) -> bool {
209 matches!(self, Error::Delta(model) if model.error_type.is_update_requirement_conflict())
210 }
211
212 pub fn is_resource_exhausted(&self) -> bool {
216 matches!(self, Error::Delta(model) if model.error_type.is_resource_exhausted())
217 }
218
219 pub fn is_commit_state_unknown(&self) -> bool {
223 matches!(self, Error::Delta(model) if model.error_type.is_commit_state_unknown())
224 }
225
226 pub fn is_unsupported_table_format(&self) -> bool {
230 matches!(self, Error::Delta(model) if model.error_type.is_unsupported_table_format())
231 }
232
233 pub fn is_not_implemented(&self) -> bool {
237 matches!(self, Error::Delta(model) if model.error_type.is_not_implemented())
238 }
239
240 pub fn is_route_missing(&self) -> bool {
250 matches!(self, Error::Api(UcApiError::Other { status: 404, .. }))
251 }
252
253 pub fn should_fall_back_to_legacy(&self) -> bool {
259 self.is_unsupported_table_format() || self.is_not_implemented() || self.is_route_missing()
260 }
261}
262
263#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone)]
265#[non_exhaustive]
266pub enum UcApiError {
267 #[error("Invalid parameter: {message}")]
268 InvalidParameter { message: String },
269
270 #[error("Unauthenticated: {message}")]
271 Unauthenticated { message: String },
272
273 #[error("Permission denied: {message}")]
274 PermissionDenied { message: String },
275
276 #[error("Resource not found: {message}")]
277 NotFound { message: String },
278
279 #[error("Resource already exists: {message}")]
280 AlreadyExists { message: String },
281
282 #[error("Request limit exceeded: {message}")]
283 RequestLimitExceeded { message: String },
284
285 #[error("Internal server error: {message}")]
286 InternalError { message: String },
287
288 #[error("Temporarily unavailable: {message}")]
289 TemporarilyUnavailable { message: String },
290
291 #[error("API error {status}: [{error_code}] {message}")]
292 Other {
293 status: u16,
294 error_code: String,
295 message: String,
296 },
297}
298
299impl UcApiError {
300 pub fn error_code(&self) -> &str {
302 match self {
303 UcApiError::InvalidParameter { .. } => "INVALID_PARAMETER_VALUE",
304 UcApiError::Unauthenticated { .. } => "UNAUTHENTICATED",
305 UcApiError::PermissionDenied { .. } => "PERMISSION_DENIED",
306 UcApiError::NotFound { .. } => "RESOURCE_NOT_FOUND",
307 UcApiError::AlreadyExists { .. } => "RESOURCE_ALREADY_EXISTS",
308 UcApiError::RequestLimitExceeded { .. } => "REQUEST_LIMIT_EXCEEDED",
309 UcApiError::InternalError { .. } => "INTERNAL_ERROR",
310 UcApiError::TemporarilyUnavailable { .. } => "TEMPORARILY_UNAVAILABLE",
311 UcApiError::Other { error_code, .. } => error_code,
312 }
313 }
314
315 pub fn http_status(&self) -> u16 {
317 match self {
318 UcApiError::InvalidParameter { .. } => 400,
319 UcApiError::Unauthenticated { .. } => 401,
320 UcApiError::PermissionDenied { .. } => 403,
321 UcApiError::NotFound { .. } => 404,
322 UcApiError::AlreadyExists { .. } => 409,
323 UcApiError::RequestLimitExceeded { .. } => 429,
324 UcApiError::InternalError { .. } => 500,
325 UcApiError::TemporarilyUnavailable { .. } => 503,
326 UcApiError::Other { status, .. } => *status,
327 }
328 }
329
330 pub fn from_api_response(status: u16, error_code: &str, message: String) -> Self {
332 match error_code {
333 "INVALID_PARAMETER_VALUE" => UcApiError::InvalidParameter { message },
334 "UNAUTHENTICATED" => UcApiError::Unauthenticated { message },
335 "PERMISSION_DENIED" => UcApiError::PermissionDenied { message },
336 "RESOURCE_NOT_FOUND" => UcApiError::NotFound { message },
337 "RESOURCE_ALREADY_EXISTS" => UcApiError::AlreadyExists { message },
338 "REQUEST_LIMIT_EXCEEDED" => UcApiError::RequestLimitExceeded { message },
339 "INTERNAL_ERROR" => UcApiError::InternalError { message },
340 "TEMPORARILY_UNAVAILABLE" => UcApiError::TemporarilyUnavailable { message },
341 other => UcApiError::Other {
342 status,
343 error_code: other.to_string(),
344 message,
345 },
346 }
347 }
348}
349
350#[derive(serde::Deserialize)]
352struct ApiErrorBody {
353 #[serde(alias = "errorCode")]
354 error_code: String,
355 message: String,
356}
357
358pub(crate) fn parse_error(status: u16, body: Option<&[u8]>) -> Error {
362 let body = body.unwrap_or_default();
363 match serde_json::from_slice::<ApiErrorBody>(body) {
364 Ok(api_err) => {
365 UcApiError::from_api_response(status, &api_err.error_code, api_err.message).into()
366 }
367 Err(_) => UcApiError::Other {
368 status,
369 error_code: String::new(),
370 message: String::from_utf8_lossy(body).into_owned(),
371 }
372 .into(),
373 }
374}
375
376pub(crate) async fn parse_error_response(response: reqwest::Response) -> Error {
381 let status = response.status().as_u16();
382 match response.bytes().await {
383 Ok(body) => parse_error(status, Some(&body)),
384 Err(e) => Error::RequestError(e),
385 }
386}
387
388pub(crate) fn parse_delta_error(status: u16, body: Option<&[u8]>) -> Error {
402 use unitycatalog_delta_api::models::DeltaErrorResponse;
403
404 let body = body.unwrap_or_default();
405 match serde_json::from_slice::<DeltaErrorResponse>(body) {
406 Ok(envelope) => Error::Delta(envelope.error),
407 Err(_) => UcApiError::Other {
408 status,
409 error_code: String::new(),
410 message: String::from_utf8_lossy(body).into_owned(),
411 }
412 .into(),
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 fn make_response(status: u16, body: &'static str) -> reqwest::Response {
421 http::Response::builder()
422 .status(status)
423 .header("content-type", "application/json")
424 .body(bytes::Bytes::from_static(body.as_bytes()))
425 .map(reqwest::Response::from)
426 .unwrap()
427 }
428
429 #[tokio::test]
430 async fn test_parse_error_resource_not_found() {
431 let resp = make_response(
432 404,
433 r#"{"error_code":"RESOURCE_NOT_FOUND","message":"catalog 'foo' not found"}"#,
434 );
435 let err = parse_error_response(resp).await;
436 assert!(err.is_not_found());
437 assert!(matches!(
438 err,
439 Error::Api(UcApiError::NotFound { ref message }) if message == "catalog 'foo' not found"
440 ));
441 }
442
443 #[tokio::test]
444 async fn test_parse_error_camel_case_alias() {
445 let resp = make_response(
446 404,
447 r#"{"errorCode":"RESOURCE_NOT_FOUND","message":"not found"}"#,
448 );
449 let err = parse_error_response(resp).await;
450 assert!(err.is_not_found());
451 }
452
453 #[tokio::test]
454 async fn test_parse_error_non_json_body() {
455 let resp = make_response(500, "Internal Server Error");
456 let err = parse_error_response(resp).await;
457 assert!(matches!(
458 err,
459 Error::Api(UcApiError::Other {
460 status: 500,
461 ref message,
462 ..
463 }) if message == "Internal Server Error"
464 ));
465 }
466
467 #[tokio::test]
468 async fn test_parse_error_already_exists() {
469 let resp = make_response(
470 409,
471 r#"{"error_code":"RESOURCE_ALREADY_EXISTS","message":"already exists"}"#,
472 );
473 let err = parse_error_response(resp).await;
474 assert!(err.is_already_exists());
475 }
476
477 fn delta_err(status: u16, body: &str) -> Error {
481 parse_delta_error(status, Some(body.as_bytes()))
482 }
483
484 #[test]
485 fn test_parse_delta_error_not_found() {
486 let err = delta_err(
487 404,
488 r#"{"error":{"message":"table 'x' not found","type":"NoSuchTableException","code":404}}"#,
489 );
490 assert!(err.is_not_found());
491 assert!(!err.is_already_exists());
492 assert!(matches!(
493 err,
494 Error::Delta(ref m) if m.message == "table 'x' not found" && m.code == 404
495 ));
496 }
497
498 #[test]
499 fn test_parse_delta_error_already_exists() {
500 let err = delta_err(
501 409,
502 r#"{"error":{"message":"exists","type":"AlreadyExistsException","code":409}}"#,
503 );
504 assert!(err.is_already_exists());
505 assert!(!err.is_not_found());
506 }
507
508 #[test]
509 fn test_parse_delta_error_commit_conflict() {
510 let err = delta_err(
511 409,
512 r#"{"error":{"message":"conflict","type":"CommitVersionConflictException","code":409}}"#,
513 );
514 assert!(err.is_commit_conflict());
515 assert!(!err.is_update_requirement_conflict());
516 assert!(!err.is_already_exists());
517 }
518
519 #[test]
520 fn test_parse_delta_error_update_requirement_conflict() {
521 let err = delta_err(
522 409,
523 r#"{"error":{"message":"etag mismatch","type":"UpdateRequirementConflictException","code":409}}"#,
524 );
525 assert!(err.is_update_requirement_conflict());
526 assert!(!err.is_commit_conflict());
527 }
528
529 #[test]
530 fn test_parse_delta_error_resource_exhausted() {
531 for ty in ["ResourceExhaustedException", "TooManyRequestsException"] {
532 let body = format!(r#"{{"error":{{"message":"slow down","type":"{ty}","code":429}}}}"#);
533 let err = delta_err(429, &body);
534 assert!(err.is_resource_exhausted(), "type {ty} should be exhausted");
535 }
536 }
537
538 #[test]
539 fn test_parse_delta_error_commit_state_unknown() {
540 let err = delta_err(
541 500,
542 r#"{"error":{"message":"unknown","type":"CommitStateUnknownException","code":500}}"#,
543 );
544 assert!(err.is_commit_state_unknown());
545 }
546
547 #[test]
548 fn test_parse_delta_error_with_stack() {
549 let err = delta_err(
550 500,
551 r#"{"error":{"message":"boom","type":"InternalServerErrorException","code":500,"stack":["a","b"]}}"#,
552 );
553 assert!(matches!(
554 err,
555 Error::Delta(ref m) if m.stack.as_deref() == Some(&["a".to_string(), "b".to_string()][..])
556 ));
557 }
558
559 #[test]
560 fn test_parse_delta_error_non_envelope_body() {
561 let err = delta_err(502, "Bad Gateway");
562 assert!(matches!(
563 err,
564 Error::Api(UcApiError::Other { status: 502, ref message, .. }) if message == "Bad Gateway"
565 ));
566 }
567
568 #[test]
569 fn test_parse_delta_error_unsupported_table_format() {
570 let err = delta_err(
573 400,
574 r#"{"error":{"message":"not a delta table","type":"UnsupportedTableFormatException","code":400}}"#,
575 );
576 assert!(err.is_unsupported_table_format());
577 assert!(err.should_fall_back_to_legacy());
578 assert!(!err.is_not_found());
579 assert!(!err.is_route_missing());
580 }
581
582 #[test]
583 fn test_parse_delta_error_not_implemented() {
584 let err = delta_err(
585 501,
586 r#"{"error":{"message":"nope","type":"NotImplementedException","code":501}}"#,
587 );
588 assert!(err.is_not_implemented());
589 assert!(err.should_fall_back_to_legacy());
590 }
591
592 #[test]
593 fn test_route_missing_is_non_envelope_404() {
594 let err = delta_err(404, "Not Found");
598 assert!(err.is_route_missing());
599 assert!(err.should_fall_back_to_legacy());
600 let enveloped = delta_err(
603 404,
604 r#"{"error":{"message":"no table","type":"NoSuchTableException","code":404}}"#,
605 );
606 assert!(enveloped.is_not_found());
607 assert!(!enveloped.is_route_missing());
608 assert!(!enveloped.should_fall_back_to_legacy());
609 }
610}