1use serde::{Deserialize, Serialize};
99use std::fmt;
100use std::sync::{Arc, LazyLock};
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
103pub enum ErrorClass {
104 InvalidArgument,
106 CannotConnect,
108 Disconnected,
110 ConnectionTimeout,
112 ResponseTimeout,
114 Cancelled,
116 ResourceExhausted,
119 WorkerOverloaded,
123 Unavailable,
125 WorkerUnavailable,
130 Backend(BackendError),
132 InvalidRequest,
134 Unauthenticated,
136 PermissionDenied,
138 NotFound,
140 Conflict,
142 PayloadTooLarge,
144 UnsupportedMedia,
146 RateLimited,
148 CapacityExhausted,
150 BackendProtocol,
152 DeadlineExceeded,
154 NotImplemented,
156 Internal,
158 Unknown,
160}
161
162#[derive(Deserialize)]
163#[serde(untagged)]
164#[allow(non_snake_case)]
165enum ErrorClassWire {
166 Named(String),
167 Backend { Backend: BackendError },
168}
169
170impl ErrorClass {
171 fn from_wire_name(name: &str) -> Self {
172 match name {
173 "InvalidArgument" => Self::InvalidArgument,
174 "CannotConnect" => Self::CannotConnect,
175 "Disconnected" => Self::Disconnected,
176 "ConnectionTimeout" => Self::ConnectionTimeout,
177 "ResponseTimeout" => Self::ResponseTimeout,
178 "Cancelled" => Self::Cancelled,
179 "ResourceExhausted" => Self::ResourceExhausted,
180 "WorkerOverloaded" => Self::WorkerOverloaded,
181 "Unavailable" => Self::Unavailable,
182 "WorkerUnavailable" => Self::WorkerUnavailable,
183 "InvalidRequest" => Self::InvalidRequest,
184 "Unauthenticated" => Self::Unauthenticated,
185 "PermissionDenied" => Self::PermissionDenied,
186 "NotFound" => Self::NotFound,
187 "Conflict" => Self::Conflict,
188 "PayloadTooLarge" => Self::PayloadTooLarge,
189 "UnsupportedMedia" => Self::UnsupportedMedia,
190 "RateLimited" => Self::RateLimited,
191 "CapacityExhausted" => Self::CapacityExhausted,
192 "BackendProtocol" => Self::BackendProtocol,
193 "DeadlineExceeded" => Self::DeadlineExceeded,
194 "NotImplemented" => Self::NotImplemented,
195 "Internal" => Self::Internal,
196 _ => Self::Unknown,
197 }
198 }
199}
200
201impl<'de> Deserialize<'de> for ErrorClass {
202 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
203 where
204 D: serde::Deserializer<'de>,
205 {
206 Ok(match ErrorClassWire::deserialize(deserializer)? {
207 ErrorClassWire::Named(name) => Self::from_wire_name(&name),
208 ErrorClassWire::Backend { Backend: error } => Self::Backend(error),
209 })
210 }
211}
212
213impl fmt::Display for ErrorClass {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 match self {
216 ErrorClass::Unknown => write!(f, "Unknown"),
217 ErrorClass::InvalidArgument => write!(f, "InvalidArgument"),
218 ErrorClass::CannotConnect => write!(f, "CannotConnect"),
219 ErrorClass::Disconnected => write!(f, "Disconnected"),
220 ErrorClass::ConnectionTimeout => write!(f, "ConnectionTimeout"),
221 ErrorClass::ResponseTimeout => write!(f, "ResponseTimeout"),
222 ErrorClass::Cancelled => write!(f, "Cancelled"),
223 ErrorClass::ResourceExhausted => write!(f, "ResourceExhausted"),
224 ErrorClass::WorkerOverloaded => write!(f, "WorkerOverloaded"),
225 ErrorClass::Unavailable => write!(f, "Unavailable"),
226 ErrorClass::WorkerUnavailable => write!(f, "WorkerUnavailable"),
227 ErrorClass::Backend(sub) => write!(f, "Backend{sub}"),
228 ErrorClass::InvalidRequest => write!(f, "InvalidRequest"),
229 ErrorClass::Unauthenticated => write!(f, "Unauthenticated"),
230 ErrorClass::PermissionDenied => write!(f, "PermissionDenied"),
231 ErrorClass::NotFound => write!(f, "NotFound"),
232 ErrorClass::Conflict => write!(f, "Conflict"),
233 ErrorClass::PayloadTooLarge => write!(f, "PayloadTooLarge"),
234 ErrorClass::UnsupportedMedia => write!(f, "UnsupportedMedia"),
235 ErrorClass::RateLimited => write!(f, "RateLimited"),
236 ErrorClass::CapacityExhausted => write!(f, "CapacityExhausted"),
237 ErrorClass::BackendProtocol => write!(f, "BackendProtocol"),
238 ErrorClass::DeadlineExceeded => write!(f, "DeadlineExceeded"),
239 ErrorClass::NotImplemented => write!(f, "NotImplemented"),
240 ErrorClass::Internal => write!(f, "Internal"),
241 }
242 }
243}
244
245impl ErrorClass {
246 pub fn as_str(self) -> &'static str {
248 match self {
249 Self::Unknown => "Unknown",
250 Self::InvalidArgument => "InvalidArgument",
251 Self::CannotConnect => "CannotConnect",
252 Self::Disconnected => "Disconnected",
253 Self::ConnectionTimeout => "ConnectionTimeout",
254 Self::ResponseTimeout => "ResponseTimeout",
255 Self::Cancelled => "Cancelled",
256 Self::ResourceExhausted => "ResourceExhausted",
257 Self::WorkerOverloaded => "WorkerOverloaded",
258 Self::Unavailable => "Unavailable",
259 Self::WorkerUnavailable => "WorkerUnavailable",
260 Self::Backend(BackendError::Unknown) => "BackendUnknown",
261 Self::Backend(BackendError::InvalidArgument) => "BackendInvalidArgument",
262 Self::Backend(BackendError::CannotConnect) => "BackendCannotConnect",
263 Self::Backend(BackendError::Disconnected) => "BackendDisconnected",
264 Self::Backend(BackendError::ConnectionTimeout) => "BackendConnectionTimeout",
265 Self::Backend(BackendError::ResponseTimeout) => "BackendResponseTimeout",
266 Self::Backend(BackendError::Cancelled) => "BackendCancelled",
267 Self::Backend(BackendError::EngineShutdown) => "BackendEngineShutdown",
268 Self::Backend(BackendError::StreamIncomplete) => "BackendStreamIncomplete",
269 Self::InvalidRequest => "InvalidRequest",
270 Self::Unauthenticated => "Unauthenticated",
271 Self::PermissionDenied => "PermissionDenied",
272 Self::NotFound => "NotFound",
273 Self::Conflict => "Conflict",
274 Self::PayloadTooLarge => "PayloadTooLarge",
275 Self::UnsupportedMedia => "UnsupportedMedia",
276 Self::RateLimited => "RateLimited",
277 Self::CapacityExhausted => "CapacityExhausted",
278 Self::BackendProtocol => "BackendProtocol",
279 Self::DeadlineExceeded => "DeadlineExceeded",
280 Self::NotImplemented => "NotImplemented",
281 Self::Internal => "Internal",
282 }
283 }
284
285 pub fn normalized(self) -> Self {
289 match self {
290 Self::Unknown => Self::Internal,
291 Self::InvalidArgument => Self::InvalidRequest,
292 Self::CannotConnect | Self::Disconnected | Self::WorkerUnavailable => Self::Unavailable,
293 Self::ConnectionTimeout | Self::ResponseTimeout => Self::DeadlineExceeded,
294 Self::ResourceExhausted | Self::WorkerOverloaded => Self::CapacityExhausted,
295 Self::Backend(error) => match error {
296 BackendError::Unknown => Self::Internal,
297 BackendError::InvalidArgument => Self::InvalidRequest,
298 BackendError::CannotConnect
299 | BackendError::Disconnected
300 | BackendError::EngineShutdown
301 | BackendError::StreamIncomplete => Self::Unavailable,
302 BackendError::ConnectionTimeout | BackendError::ResponseTimeout => {
303 Self::DeadlineExceeded
304 }
305 BackendError::Cancelled => Self::Cancelled,
306 },
307 canonical @ (Self::Cancelled
308 | Self::Unavailable
309 | Self::InvalidRequest
310 | Self::Unauthenticated
311 | Self::PermissionDenied
312 | Self::NotFound
313 | Self::Conflict
314 | Self::PayloadTooLarge
315 | Self::UnsupportedMedia
316 | Self::RateLimited
317 | Self::CapacityExhausted
318 | Self::BackendProtocol
319 | Self::DeadlineExceeded
320 | Self::NotImplemented
321 | Self::Internal) => canonical,
322 }
323 }
324}
325
326pub type ErrorType = ErrorClass;
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
335pub enum BackendError {
336 Unknown,
338 InvalidArgument,
340 CannotConnect,
342 Disconnected,
344 ConnectionTimeout,
346 ResponseTimeout,
348 Cancelled,
350 EngineShutdown,
352 StreamIncomplete,
354}
355
356impl fmt::Display for BackendError {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 match self {
359 BackendError::Unknown => write!(f, "Unknown"),
360 BackendError::InvalidArgument => write!(f, "InvalidArgument"),
361 BackendError::CannotConnect => write!(f, "CannotConnect"),
362 BackendError::Disconnected => write!(f, "Disconnected"),
363 BackendError::ConnectionTimeout => write!(f, "ConnectionTimeout"),
364 BackendError::ResponseTimeout => write!(f, "ResponseTimeout"),
365 BackendError::Cancelled => write!(f, "Cancelled"),
366 BackendError::EngineShutdown => write!(f, "EngineShutdown"),
367 BackendError::StreamIncomplete => write!(f, "StreamIncomplete"),
368 }
369 }
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Hash)]
374pub struct ErrorReason(String);
375
376impl ErrorReason {
377 pub const MAX_BYTES: usize = 128;
378
379 pub fn new(value: impl Into<String>) -> Result<Self, InvalidErrorReason> {
380 let value = value.into();
381 if value.is_empty() {
382 return Err(InvalidErrorReason::Empty);
383 }
384 if value.len() > Self::MAX_BYTES {
385 return Err(InvalidErrorReason::TooLong);
386 }
387 if !value.bytes().all(|byte| {
388 byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"._-".contains(&byte)
389 }) {
390 return Err(InvalidErrorReason::InvalidCharacter);
391 }
392 if Self::catalog_class(&value).is_none() {
393 return Err(InvalidErrorReason::UnknownCatalogKey);
394 }
395 Ok(Self(value))
396 }
397
398 pub fn as_str(&self) -> &str {
399 &self.0
400 }
401
402 fn catalog_class(value: &str) -> Option<ErrorClass> {
403 match value {
404 "runtime.unclassified" | "runtime.invalid_error" | "runtime.internal" => {
405 Some(ErrorClass::Internal)
406 }
407 "request.invalid_argument" | "backend.invalid_argument" | "request.invalid" => {
408 Some(ErrorClass::InvalidRequest)
409 }
410 "transport.cannot_connect"
411 | "transport.disconnected"
412 | "backend.unavailable"
413 | "backend.worker_unavailable"
414 | "backend.cannot_connect"
415 | "backend.disconnected"
416 | "backend.engine_shutdown"
417 | "backend.stream_incomplete" => Some(ErrorClass::Unavailable),
418 "transport.connection_timeout"
419 | "backend.response_timeout"
420 | "backend.connection_timeout"
421 | "request.deadline_exceeded"
422 | "router.queue_deadline_exceeded" => Some(ErrorClass::DeadlineExceeded),
423 "request.cancelled" | "backend.cancelled" => Some(ErrorClass::Cancelled),
424 "capacity.pool_exhausted" | "capacity.worker_overloaded" | "capacity.exhausted" => {
425 Some(ErrorClass::CapacityExhausted)
426 }
427 "backend.unknown" => Some(ErrorClass::Internal),
428 "backend.protocol" => Some(ErrorClass::BackendProtocol),
429 "request.unauthenticated" => Some(ErrorClass::Unauthenticated),
430 "request.permission_denied" => Some(ErrorClass::PermissionDenied),
431 "request.not_found" => Some(ErrorClass::NotFound),
432 "request.conflict" => Some(ErrorClass::Conflict),
433 "request.payload_too_large" => Some(ErrorClass::PayloadTooLarge),
434 "request.unsupported_media" => Some(ErrorClass::UnsupportedMedia),
435 "request.rate_limited" => Some(ErrorClass::RateLimited),
436 "runtime.not_implemented" => Some(ErrorClass::NotImplemented),
437 _ => None,
438 }
439 }
440
441 fn from_static(value: &'static str) -> Self {
442 debug_assert!(!value.is_empty() && value.len() <= Self::MAX_BYTES);
443 debug_assert!(value.bytes().all(|byte| {
444 byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"._-".contains(&byte)
445 }));
446 debug_assert!(Self::catalog_class(value).is_some());
447 Self(value.to_owned())
448 }
449
450 fn for_class(class: ErrorClass) -> Self {
451 let value = match class {
452 ErrorClass::Unknown => "runtime.unclassified",
453 ErrorClass::InvalidArgument => "request.invalid_argument",
454 ErrorClass::CannotConnect => "transport.cannot_connect",
455 ErrorClass::Disconnected => "transport.disconnected",
456 ErrorClass::ConnectionTimeout => "transport.connection_timeout",
457 ErrorClass::ResponseTimeout => "backend.response_timeout",
458 ErrorClass::Cancelled => "request.cancelled",
459 ErrorClass::ResourceExhausted => "capacity.pool_exhausted",
460 ErrorClass::WorkerOverloaded => "capacity.worker_overloaded",
461 ErrorClass::Unavailable => "backend.unavailable",
462 ErrorClass::WorkerUnavailable => "backend.worker_unavailable",
463 ErrorClass::Backend(error) => match error {
464 BackendError::Unknown => "backend.unknown",
465 BackendError::InvalidArgument => "backend.invalid_argument",
466 BackendError::CannotConnect => "backend.cannot_connect",
467 BackendError::Disconnected => "backend.disconnected",
468 BackendError::ConnectionTimeout => "backend.connection_timeout",
469 BackendError::ResponseTimeout => "backend.response_timeout",
470 BackendError::Cancelled => "backend.cancelled",
471 BackendError::EngineShutdown => "backend.engine_shutdown",
472 BackendError::StreamIncomplete => "backend.stream_incomplete",
473 },
474 ErrorClass::InvalidRequest => "request.invalid",
475 ErrorClass::Unauthenticated => "request.unauthenticated",
476 ErrorClass::PermissionDenied => "request.permission_denied",
477 ErrorClass::NotFound => "request.not_found",
478 ErrorClass::Conflict => "request.conflict",
479 ErrorClass::PayloadTooLarge => "request.payload_too_large",
480 ErrorClass::UnsupportedMedia => "request.unsupported_media",
481 ErrorClass::RateLimited => "request.rate_limited",
482 ErrorClass::CapacityExhausted => "capacity.exhausted",
483 ErrorClass::BackendProtocol => "backend.protocol",
484 ErrorClass::DeadlineExceeded => "request.deadline_exceeded",
485 ErrorClass::NotImplemented => "runtime.not_implemented",
486 ErrorClass::Internal => "runtime.internal",
487 };
488 Self::from_static(value)
489 }
490}
491
492static INVALID_ERROR_REASON: LazyLock<ErrorReason> =
493 LazyLock::new(|| ErrorReason::from_static("runtime.invalid_error"));
494
495impl fmt::Display for ErrorReason {
496 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
497 f.write_str(&self.0)
498 }
499}
500
501impl Serialize for ErrorReason {
502 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
503 where
504 S: serde::Serializer,
505 {
506 serializer.serialize_str(&self.0)
507 }
508}
509
510impl<'de> Deserialize<'de> for ErrorReason {
511 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
512 where
513 D: serde::Deserializer<'de>,
514 {
515 let value = String::deserialize(deserializer)?;
516 Self::new(value).map_err(serde::de::Error::custom)
517 }
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq)]
521pub enum InvalidErrorReason {
522 Empty,
523 TooLong,
524 InvalidCharacter,
525 UnknownCatalogKey,
526}
527
528impl fmt::Display for InvalidErrorReason {
529 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
530 match self {
531 Self::Empty => f.write_str("error reason cannot be empty"),
532 Self::TooLong => write!(f, "error reason exceeds {} bytes", ErrorReason::MAX_BYTES),
533 Self::InvalidCharacter => f.write_str(
534 "error reason may contain only lowercase ASCII, digits, '.', '_', or '-'",
535 ),
536 Self::UnknownCatalogKey => f.write_str("error reason is not registered in the catalog"),
537 }
538 }
539}
540
541impl std::error::Error for InvalidErrorReason {}
542
543#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
545#[serde(tag = "type", rename_all = "snake_case")]
546pub enum PublicDetails {
547 Message {
548 message: String,
549 },
550 SizeLimit {
551 limit: u64,
552 actual: Option<u64>,
553 },
554 ContextLength {
555 limit: u64,
556 actual: Option<u64>,
557 },
558 RateLimit {
559 limit: Option<u64>,
560 remaining: Option<u64>,
561 },
562}
563
564impl PublicDetails {
565 pub fn message(&self) -> Option<&str> {
567 match self {
568 Self::Message { message } => Some(message),
569 Self::SizeLimit { .. } | Self::ContextLength { .. } | Self::RateLimit { .. } => None,
570 }
571 }
572}
573
574fn bounded_message(mut message: String) -> String {
575 if message.len() > Diagnostic::MAX_BYTES {
576 let mut end = Diagnostic::MAX_BYTES - Diagnostic::TRUNCATION_SUFFIX.len();
577 while !message.is_char_boundary(end) {
578 end -= 1;
579 }
580 message.truncate(end);
581 message.push_str(Diagnostic::TRUNCATION_SUFFIX);
582 }
583 message
584}
585
586#[derive(Debug, Clone, Default)]
588pub struct Diagnostic {
589 message: String,
590 source: Option<Arc<dyn std::error::Error + Send + Sync>>,
591}
592
593impl Diagnostic {
594 pub const MAX_BYTES: usize = 4096;
595 pub const TRUNCATION_SUFFIX: &'static str = "...[truncated]";
596
597 pub fn new(value: impl Into<String>) -> Self {
598 Self {
599 message: bounded_message(value.into()),
600 source: None,
601 }
602 }
603
604 fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
605 self.source = Some(Arc::new(source));
606 self
607 }
608
609 pub fn as_str(&self) -> &str {
610 &self.message
611 }
612
613 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
614 self.source
615 .as_deref()
616 .map(|source| source as &(dyn std::error::Error + 'static))
617 }
618}
619
620impl PartialEq for Diagnostic {
621 fn eq(&self, other: &Self) -> bool {
622 self.message == other.message
623 }
624}
625
626impl Eq for Diagnostic {}
627
628impl Serialize for Diagnostic {
629 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
630 where
631 S: serde::Serializer,
632 {
633 serializer.serialize_str(&self.message)
634 }
635}
636
637impl<'de> Deserialize<'de> for Diagnostic {
638 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
639 where
640 D: serde::Deserializer<'de>,
641 {
642 Ok(Self::new(String::deserialize(deserializer)?))
643 }
644}
645
646#[derive(Debug, Clone, PartialEq, Eq)]
666pub struct DynamoError {
667 pub class: ErrorClass,
669 pub reason: ErrorReason,
671 pub diagnostic: Option<Diagnostic>,
673 pub public: Option<PublicDetails>,
675}
676
677impl Serialize for DynamoError {
678 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
679 where
680 S: serde::Serializer,
681 {
682 use serde::ser::SerializeStruct;
683
684 let public = self.public_details();
685 let caused_by = self
686 .diagnostic
687 .as_ref()
688 .and_then(Diagnostic::source)
689 .and_then(|source| source.downcast_ref::<DynamoError>());
690 let mut state = serializer.serialize_struct(
691 "DynamoError",
692 4 + usize::from(self.diagnostic.is_some())
693 + usize::from(public.is_some())
694 + usize::from(caused_by.is_some()),
695 )?;
696 state.serialize_field("error_type", &self.legacy_wire_error_type())?;
697 state.serialize_field("class", &self.class())?;
698 state.serialize_field("reason", self.reason())?;
699 state.serialize_field("message", &self.message())?;
700 if let Some(diagnostic) = &self.diagnostic {
701 state.serialize_field("diagnostic", diagnostic)?;
702 }
703 if let Some(public) = public {
704 state.serialize_field("public", public)?;
705 }
706 if let Some(caused_by) = caused_by {
707 state.serialize_field("caused_by", caused_by)?;
708 }
709 state.end()
710 }
711}
712
713impl<'de> Deserialize<'de> for DynamoError {
714 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
715 where
716 D: serde::Deserializer<'de>,
717 {
718 #[derive(Deserialize)]
719 struct Representation {
720 #[serde(default)]
721 class: Option<ErrorClass>,
722 #[serde(default)]
723 error_type: Option<ErrorClass>,
724 #[serde(default)]
725 reason: Option<String>,
726 #[serde(default)]
727 diagnostic: Option<Diagnostic>,
728 #[serde(default)]
729 message: Option<Diagnostic>,
730 #[serde(default, rename = "public", alias = "public_details")]
731 public: Option<PublicDetails>,
732 #[serde(default)]
733 caused_by: Option<Box<DynamoError>>,
734 }
735
736 let representation = Representation::deserialize(deserializer)?;
737 let declared_class = representation.class;
738 let invalid_declared_class = matches!(declared_class, Some(ErrorClass::Unknown));
739 let legacy_error_type = representation.error_type;
740 let source_class = declared_class
741 .or(legacy_error_type)
742 .unwrap_or(ErrorClass::Unknown);
743 let canonical_class = match source_class {
744 ErrorClass::Unknown => ErrorClass::Internal,
745 class => class.normalized(),
746 };
747 let reason = match representation.reason {
748 Some(reason) => ErrorReason::new(reason).ok(),
749 None => Some(ErrorReason::for_class(
750 legacy_error_type.unwrap_or(source_class),
751 )),
752 };
753 let valid_reason = !invalid_declared_class
754 && reason
755 .as_ref()
756 .and_then(|reason| ErrorReason::catalog_class(reason.as_str()))
757 .is_some_and(|class| class == canonical_class);
758 let stored_class = legacy_error_type
759 .filter(|class| *class != ErrorClass::Unknown && class.normalized() == canonical_class)
760 .unwrap_or(canonical_class);
761
762 let (class, reason, public) = match reason {
763 Some(reason) if valid_reason => (stored_class, reason, representation.public),
764 _ => (
765 ErrorClass::Internal,
766 ErrorReason::from_static("runtime.invalid_error"),
767 None,
768 ),
769 };
770
771 let diagnostic = match (
772 representation.diagnostic.or(representation.message),
773 representation.caused_by,
774 ) {
775 (Some(diagnostic), Some(caused_by)) => Some(diagnostic.with_source(*caused_by)),
776 (None, Some(caused_by)) => Some(Diagnostic::new("").with_source(*caused_by)),
777 (diagnostic, None) => diagnostic,
778 };
779
780 Ok(Self {
781 class,
782 reason,
783 diagnostic,
784 public,
785 })
786 }
787}
788
789impl DynamoError {
790 pub fn builder() -> DynamoErrorBuilder {
792 DynamoErrorBuilder::default()
793 }
794
795 pub fn msg(message: impl Into<String>) -> Self {
797 Self::builder()
798 .class(ErrorClass::Internal)
799 .reason(ErrorReason::from_static("runtime.unclassified"))
800 .diagnostic(message)
801 .build()
802 }
803
804 pub fn error_type(&self) -> ErrorType {
809 if self.has_valid_identity() {
810 self.class
811 } else {
812 ErrorClass::Internal
813 }
814 }
815
816 fn has_valid_identity(&self) -> bool {
817 ErrorReason::catalog_class(self.reason.as_str())
818 .is_some_and(|class| class == self.class.normalized())
819 }
820
821 pub fn class(&self) -> ErrorClass {
823 if self.has_valid_identity() {
824 self.class.normalized()
825 } else {
826 ErrorClass::Internal
827 }
828 }
829
830 fn legacy_wire_error_type(&self) -> ErrorClass {
831 match self.error_type() {
832 ErrorClass::Unknown
833 | ErrorClass::Internal
834 | ErrorClass::Unauthenticated
835 | ErrorClass::PermissionDenied
836 | ErrorClass::NotFound
837 | ErrorClass::Conflict
838 | ErrorClass::BackendProtocol
839 | ErrorClass::NotImplemented => ErrorClass::Unknown,
840 ErrorClass::InvalidArgument
841 | ErrorClass::InvalidRequest
842 | ErrorClass::PayloadTooLarge
843 | ErrorClass::UnsupportedMedia => ErrorClass::InvalidArgument,
844 ErrorClass::CannotConnect => ErrorClass::CannotConnect,
845 ErrorClass::Disconnected => ErrorClass::Disconnected,
846 ErrorClass::ConnectionTimeout => ErrorClass::ConnectionTimeout,
847 ErrorClass::ResponseTimeout | ErrorClass::DeadlineExceeded => {
848 ErrorClass::ResponseTimeout
849 }
850 ErrorClass::Cancelled => ErrorClass::Cancelled,
851 ErrorClass::ResourceExhausted
852 | ErrorClass::WorkerOverloaded
853 | ErrorClass::CapacityExhausted
854 | ErrorClass::RateLimited => ErrorClass::ResourceExhausted,
855 ErrorClass::Unavailable => ErrorClass::Unavailable,
856 ErrorClass::WorkerUnavailable => ErrorClass::WorkerUnavailable,
857 ErrorClass::Backend(error) => ErrorClass::Backend(error),
858 }
859 }
860
861 pub fn reason(&self) -> &ErrorReason {
863 if self.has_valid_identity() {
864 &self.reason
865 } else {
866 &INVALID_ERROR_REASON
867 }
868 }
869
870 pub fn diagnostic(&self) -> Option<&Diagnostic> {
872 self.diagnostic.as_ref()
873 }
874
875 pub fn public_details(&self) -> Option<&PublicDetails> {
877 self.has_valid_identity()
878 .then_some(self.public.as_ref())
879 .flatten()
880 }
881
882 pub fn public_message(&self) -> Option<&str> {
884 self.public_details().and_then(PublicDetails::message)
885 }
886
887 pub fn message(&self) -> &str {
889 self.diagnostic
890 .as_ref()
891 .map(Diagnostic::as_str)
892 .unwrap_or_default()
893 }
894}
895
896impl fmt::Display for DynamoError {
897 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
898 match self.diagnostic() {
899 Some(diagnostic) if !diagnostic.as_str().is_empty() => {
900 write!(f, "{}: {}", self.class(), diagnostic.as_str())
901 }
902 _ => write!(f, "{}: {}", self.class(), self.reason()),
903 }
904 }
905}
906
907impl std::error::Error for DynamoError {
908 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
909 self.diagnostic.as_ref().and_then(Diagnostic::source)
910 }
911}
912
913impl<'a> From<&'a (dyn std::error::Error + 'static)> for DynamoError {
915 fn from(err: &'a (dyn std::error::Error + 'static)) -> Self {
916 if let Some(dynamo_err) = err.downcast_ref::<DynamoError>() {
917 return dynamo_err.clone();
918 }
919
920 let diagnostic = Diagnostic::new(err.to_string());
921 let Some(source) = err.source() else {
922 return Self {
923 class: ErrorClass::Internal,
924 reason: ErrorReason::from_static("runtime.unclassified"),
925 diagnostic: Some(diagnostic),
926 public: None,
927 };
928 };
929
930 let source = DynamoError::from(source);
931 let diagnostic = diagnostic.with_source(source.clone());
932 if source.has_valid_identity() && source.class() != ErrorClass::Internal {
933 return Self {
934 class: source.error_type(),
935 reason: source.reason().clone(),
936 diagnostic: Some(diagnostic),
937 public: source.public_details().cloned(),
938 };
939 }
940
941 Self {
942 class: ErrorClass::Internal,
943 reason: ErrorReason::from_static("runtime.unclassified"),
944 diagnostic: Some(diagnostic),
945 public: None,
946 }
947 }
948}
949
950impl From<Box<dyn std::error::Error + 'static>> for DynamoError {
952 fn from(err: Box<dyn std::error::Error + 'static>) -> Self {
953 match err.downcast::<DynamoError>() {
954 Ok(dynamo_err) => *dynamo_err,
955 Err(err) => DynamoError::from(&*err as &(dyn std::error::Error + 'static)),
956 }
957 }
958}
959
960#[derive(Default)]
975pub struct DynamoErrorBuilder {
976 class: Option<ErrorClass>,
977 reason: Option<ErrorReason>,
978 diagnostic: Option<Diagnostic>,
979 public: Option<PublicDetails>,
980}
981
982impl DynamoErrorBuilder {
983 pub fn error_type(mut self, error_type: ErrorType) -> Self {
985 self.class = Some(error_type);
986 self
987 }
988
989 pub fn class(self, class: ErrorClass) -> Self {
991 self.error_type(class)
992 }
993
994 pub fn reason(mut self, reason: ErrorReason) -> Self {
996 self.reason = Some(reason);
997 self
998 }
999
1000 pub fn diagnostic(mut self, diagnostic: impl Into<String>) -> Self {
1002 self.diagnostic = Some(Diagnostic::new(diagnostic));
1003 self
1004 }
1005
1006 pub fn message(self, message: impl Into<String>) -> Self {
1008 self.diagnostic(message)
1009 }
1010
1011 pub fn public_message(mut self, message: impl Into<String>) -> Self {
1013 let message = bounded_message(message.into());
1014 self.public = Some(PublicDetails::Message { message });
1015 self
1016 }
1017
1018 pub fn public_details(mut self, public: PublicDetails) -> Self {
1020 self.public = Some(public);
1021 self
1022 }
1023
1024 pub fn cause(mut self, cause: impl std::error::Error + 'static) -> Self {
1026 let message = cause.to_string();
1027 let source = DynamoError::from(&cause as &(dyn std::error::Error + 'static));
1028 let diagnostic = self
1029 .diagnostic
1030 .take()
1031 .unwrap_or_else(|| Diagnostic::new(message));
1032 self.diagnostic = Some(diagnostic.with_source(source));
1033 self
1034 }
1035
1036 pub fn build(self) -> DynamoError {
1038 let raw_class = self.class.unwrap_or(ErrorClass::Internal);
1039 let reason = self
1040 .reason
1041 .unwrap_or_else(|| ErrorReason::for_class(raw_class));
1042 let valid_reason = ErrorReason::catalog_class(reason.as_str())
1043 .is_some_and(|class| class == raw_class.normalized());
1044
1045 if valid_reason {
1046 DynamoError {
1047 class: raw_class,
1048 reason,
1049 diagnostic: self.diagnostic,
1050 public: self.public,
1051 }
1052 } else {
1053 DynamoError {
1054 class: ErrorClass::Internal,
1055 reason: ErrorReason::from_static("runtime.invalid_error"),
1056 diagnostic: self.diagnostic,
1057 public: None,
1058 }
1059 }
1060 }
1061}
1062
1063pub fn match_error_chain(
1075 err: &(dyn std::error::Error + 'static),
1076 match_set: &[ErrorClass],
1077 exclude_set: &[ErrorClass],
1078) -> bool {
1079 let mut found = false;
1080 let mut current: Option<&(dyn std::error::Error + 'static)> = Some(err);
1081
1082 while let Some(e) = current {
1083 if let Some(dynamo_err) = e.downcast_ref::<DynamoError>() {
1084 if exclude_set.contains(&dynamo_err.error_type()) {
1085 return false;
1086 }
1087 if match_set.contains(&dynamo_err.error_type()) {
1088 found = true;
1089 }
1090 }
1091 current = e.source();
1092 }
1093
1094 found
1095}
1096
1097#[cfg(test)]
1102mod tests {
1103 use super::*;
1104 use std::error::Error;
1105
1106 const _: () = {
1109 fn assert_stderror<T: std::error::Error>() {}
1110 fn assert_send<T: Send>() {}
1111 fn assert_sync<T: Sync>() {}
1112 fn assert_static<T: 'static>() {}
1113 fn assert_all() {
1114 assert_stderror::<DynamoError>();
1115 assert_send::<DynamoError>();
1116 assert_sync::<DynamoError>();
1117 assert_static::<DynamoError>();
1118 }
1119 };
1120
1121 #[test]
1122 fn test_msg_constructor() {
1123 let err = DynamoError::msg("something failed");
1124 assert_eq!(err.error_type(), ErrorClass::Internal);
1125 assert_eq!(err.reason().as_str(), "runtime.unclassified");
1126 assert_eq!(err.message(), "something failed");
1127 assert!(err.source().is_none());
1128 }
1129
1130 #[test]
1131 fn cause_is_preserved_for_legacy_wire_compatibility() {
1132 let err = DynamoError::builder()
1133 .class(ErrorClass::Internal)
1134 .reason(ErrorReason::new("runtime.internal").unwrap())
1135 .diagnostic("operation failed")
1136 .cause(std::io::Error::other("io error"))
1137 .build();
1138
1139 assert_eq!(err.message(), "operation failed");
1140 assert_eq!(err.source().unwrap().to_string(), "Internal: io error");
1141 let value = serde_json::to_value(&err).unwrap();
1142 assert_eq!(value.as_object().unwrap().len(), 6);
1143 assert!(value.get("caused_by").is_some());
1144
1145 let roundtrip: DynamoError = serde_json::from_value(value).unwrap();
1146 assert_eq!(
1147 roundtrip.source().unwrap().to_string(),
1148 "Internal: io error"
1149 );
1150 }
1151
1152 #[test]
1153 fn display_uses_diagnostic_or_reason() {
1154 let with_diagnostic = DynamoError::builder()
1155 .class(ErrorClass::Internal)
1156 .reason(ErrorReason::new("runtime.internal").unwrap())
1157 .diagnostic("operation failed")
1158 .build();
1159 let without_diagnostic = DynamoError::builder()
1160 .class(ErrorClass::Internal)
1161 .reason(ErrorReason::new("runtime.internal").unwrap())
1162 .build();
1163
1164 assert_eq!(with_diagnostic.to_string(), "Internal: operation failed");
1165 assert_eq!(without_diagnostic.to_string(), "Internal: runtime.internal");
1166 }
1167
1168 #[test]
1169 fn conversion_preserves_nested_semantic_source_locally() {
1170 #[derive(Debug)]
1171 struct OuterError {
1172 source: DynamoError,
1173 }
1174
1175 impl fmt::Display for OuterError {
1176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1177 f.write_str("outer failure")
1178 }
1179 }
1180
1181 impl std::error::Error for OuterError {
1182 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1183 Some(&self.source)
1184 }
1185 }
1186
1187 let outer = OuterError {
1188 source: DynamoError::builder()
1189 .error_type(ErrorType::InvalidArgument)
1190 .diagnostic("invalid input")
1191 .build(),
1192 };
1193 let converted = DynamoError::from(&outer as &(dyn std::error::Error + 'static));
1194
1195 assert!(match_error_chain(
1196 &converted,
1197 &[ErrorType::InvalidArgument],
1198 &[]
1199 ));
1200 let value = serde_json::to_value(converted).unwrap();
1201 assert!(value.get("caused_by").is_some());
1202 }
1203
1204 #[test]
1205 fn test_from_boxed_std_error() {
1206 let std_err = std::io::Error::other("io error");
1207 let boxed: Box<dyn std::error::Error> = Box::new(std_err);
1208 let dynamo_err = DynamoError::from(boxed);
1209
1210 assert_eq!(dynamo_err.class(), ErrorClass::Internal);
1211 assert_eq!(dynamo_err.reason().as_str(), "runtime.unclassified");
1212 assert_eq!(dynamo_err.message(), "io error");
1213 }
1214
1215 #[test]
1216 fn test_from_boxed_takes_ownership_of_dynamo_error() {
1217 let inner = DynamoError::msg("original");
1218 let boxed: Box<dyn std::error::Error> = Box::new(inner);
1219 let dynamo_err = DynamoError::from(boxed);
1220
1221 assert_eq!(dynamo_err.class(), ErrorClass::Internal);
1222 assert_eq!(dynamo_err.message(), "original");
1223 }
1224
1225 #[test]
1226 fn semantic_metadata_roundtrips() {
1227 let err = DynamoError::builder()
1228 .class(ErrorClass::RateLimited)
1229 .reason(ErrorReason::new("request.rate_limited").unwrap())
1230 .diagnostic("request rate exceeded")
1231 .public_details(PublicDetails::RateLimit {
1232 limit: Some(100),
1233 remaining: Some(0),
1234 })
1235 .build();
1236
1237 let json = serde_json::to_string(&err).unwrap();
1238 let deserialized: DynamoError = serde_json::from_str(&json).unwrap();
1239
1240 assert_eq!(deserialized.class(), ErrorClass::RateLimited);
1241 assert_eq!(deserialized.reason().as_str(), "request.rate_limited");
1242 assert_eq!(
1243 deserialized.diagnostic().map(Diagnostic::as_str),
1244 Some("request rate exceeded")
1245 );
1246 assert_eq!(
1247 deserialized.public_details(),
1248 Some(&PublicDetails::RateLimit {
1249 limit: Some(100),
1250 remaining: Some(0),
1251 })
1252 );
1253 }
1254
1255 #[test]
1256 fn semantic_schema_includes_legacy_compatibility_fields() {
1257 let err = DynamoError::builder()
1258 .class(ErrorClass::RateLimited)
1259 .reason(ErrorReason::new("request.rate_limited").unwrap())
1260 .diagnostic("request rate exceeded")
1261 .public_details(PublicDetails::RateLimit {
1262 limit: Some(100),
1263 remaining: Some(0),
1264 })
1265 .build();
1266
1267 let value = serde_json::to_value(err).unwrap();
1268 let object = value.as_object().unwrap();
1269
1270 assert_eq!(object.len(), 6);
1271 assert_eq!(value["error_type"], "ResourceExhausted");
1272 assert_eq!(value["message"], "request rate exceeded");
1273 assert_eq!(value["class"], "RateLimited");
1274 assert_eq!(value["reason"], "request.rate_limited");
1275 assert_eq!(value["diagnostic"], "request rate exceeded");
1276 assert!(value.get("public").is_some());
1277 }
1278
1279 #[test]
1280 fn optional_semantic_fields_are_omitted() {
1281 let err = DynamoError::builder()
1282 .class(ErrorClass::InvalidRequest)
1283 .reason(ErrorReason::new("request.invalid").unwrap())
1284 .build();
1285
1286 let value = serde_json::to_value(err).unwrap();
1287 let object = value.as_object().unwrap();
1288
1289 assert_eq!(object.len(), 4);
1290 assert_eq!(value["error_type"], "InvalidArgument");
1291 assert_eq!(value["message"], "");
1292 assert_eq!(value["class"], "InvalidRequest");
1293 assert_eq!(value["reason"], "request.invalid");
1294 assert!(value.get("diagnostic").is_none());
1295 assert!(value.get("public").is_none());
1296 }
1297
1298 #[test]
1299 fn unknown_catalog_reason_fails_closed() {
1300 let json = r#"{
1301 "class": "InvalidRequest",
1302 "reason": "request.user_supplied_metric_label",
1303 "diagnostic": "private details"
1304 }"#;
1305 let err: DynamoError = serde_json::from_str(json).unwrap();
1306
1307 assert_eq!(err.class(), ErrorClass::Internal);
1308 assert_eq!(err.reason().as_str(), "runtime.invalid_error");
1309 assert_eq!(
1310 err.diagnostic().map(Diagnostic::as_str),
1311 Some("private details")
1312 );
1313 }
1314
1315 #[test]
1316 fn public_fields_fail_closed_at_consumer_boundaries() {
1317 let err = DynamoError {
1318 class: ErrorClass::InvalidRequest,
1319 reason: ErrorReason::new("request.rate_limited").unwrap(),
1320 diagnostic: Some(Diagnostic::new("private details")),
1321 public: Some(PublicDetails::RateLimit {
1322 limit: Some(10),
1323 remaining: Some(0),
1324 }),
1325 };
1326
1327 assert_eq!(err.class(), ErrorClass::Internal);
1328 assert_eq!(err.error_type(), ErrorClass::Internal);
1329 assert_eq!(err.reason().as_str(), "runtime.invalid_error");
1330 assert!(err.public_details().is_none());
1331 assert_eq!(err.to_string(), "Internal: private details");
1332 assert_eq!(
1333 serde_json::to_value(err).unwrap(),
1334 serde_json::json!({
1335 "error_type": "Unknown",
1336 "message": "private details",
1337 "class": "Internal",
1338 "reason": "runtime.invalid_error",
1339 "diagnostic": "private details"
1340 })
1341 );
1342 }
1343
1344 #[test]
1345 fn diagnostic_is_bounded_at_utf8_boundary() {
1346 let truncation_index = Diagnostic::MAX_BYTES - Diagnostic::TRUNCATION_SUFFIX.len();
1347 let diagnostic = Diagnostic::new(
1348 "x".repeat(truncation_index - 1) + "é" + &"x".repeat(Diagnostic::MAX_BYTES),
1349 );
1350
1351 assert!(diagnostic.as_str().len() <= Diagnostic::MAX_BYTES);
1352 assert!(
1353 diagnostic
1354 .as_str()
1355 .is_char_boundary(diagnostic.as_str().len())
1356 );
1357 assert!(diagnostic.as_str().ends_with(Diagnostic::TRUNCATION_SUFFIX));
1358 }
1359
1360 #[test]
1361 fn public_message_is_bounded_at_utf8_boundary() {
1362 let truncation_index = Diagnostic::MAX_BYTES - Diagnostic::TRUNCATION_SUFFIX.len();
1363 let message = "x".repeat(truncation_index - 1) + "é" + &"x".repeat(Diagnostic::MAX_BYTES);
1364 let error = DynamoError::builder()
1365 .class(ErrorClass::InvalidRequest)
1366 .public_message(message)
1367 .build();
1368 let public_message = error.public_message().unwrap();
1369
1370 assert!(public_message.len() <= Diagnostic::MAX_BYTES);
1371 assert!(public_message.is_char_boundary(public_message.len()));
1372 assert!(public_message.ends_with(Diagnostic::TRUNCATION_SUFFIX));
1373 }
1374
1375 #[test]
1376 fn legacy_builder_derives_semantic_defaults() {
1377 let legacy_type: ErrorType = ErrorType::InvalidArgument;
1378 let err = DynamoError::builder()
1379 .error_type(legacy_type)
1380 .message("bad request")
1381 .build();
1382
1383 assert_eq!(err.error_type(), ErrorType::InvalidArgument);
1384 assert_eq!(err.class(), ErrorClass::InvalidRequest);
1385 assert_eq!(err.reason().as_str(), "request.invalid_argument");
1386 assert_eq!(err.message(), "bad request");
1387 }
1388
1389 #[test]
1390 fn legacy_json_derives_semantic_defaults() {
1391 let json = r#"{"error_type":"InvalidArgument","message":"bad request"}"#;
1392 let err: DynamoError = serde_json::from_str(json).unwrap();
1393
1394 assert_eq!(err.class(), ErrorClass::InvalidRequest);
1395 assert_eq!(err.reason().as_str(), "request.invalid_argument");
1396 assert_eq!(
1397 err.diagnostic().map(Diagnostic::as_str),
1398 Some("bad request")
1399 );
1400 assert!(err.public_details().is_none());
1401 }
1402
1403 #[test]
1404 fn legacy_unknown_preserves_nested_classification_across_the_wire() {
1405 let json = r#"{
1406 "error_type":"Unknown",
1407 "message":"generate failed",
1408 "caused_by":{"error_type":"InvalidArgument","message":"bad request"}
1409 }"#;
1410 let err: DynamoError = serde_json::from_str(json).unwrap();
1411
1412 assert_eq!(err.reason().as_str(), "runtime.unclassified");
1413 assert_eq!(err.class(), ErrorClass::Internal);
1414 assert!(match_error_chain(&err, &[ErrorClass::InvalidArgument], &[]));
1415
1416 let reserialized = serde_json::to_value(&err).unwrap();
1417 assert_eq!(reserialized["caused_by"]["error_type"], "InvalidArgument");
1418 }
1419
1420 #[test]
1421 fn backend_class_roundtrips_as_its_canonical_class() {
1422 let error = DynamoError::builder()
1423 .error_type(ErrorClass::Backend(BackendError::InvalidArgument))
1424 .build();
1425
1426 let serialized = serde_json::to_string(&error).unwrap();
1427 assert!(serialized.contains("\"class\":\"InvalidRequest\""));
1428
1429 let decoded: DynamoError = serde_json::from_str(&serialized).unwrap();
1430 assert_eq!(
1431 decoded.error_type(),
1432 ErrorClass::Backend(BackendError::InvalidArgument)
1433 );
1434 assert_eq!(decoded.class(), ErrorClass::InvalidRequest);
1435 assert_eq!(decoded.reason().as_str(), "backend.invalid_argument");
1436 }
1437
1438 #[test]
1439 fn transport_subtype_roundtrips() {
1440 let error = DynamoError::builder()
1441 .error_type(ErrorClass::CannotConnect)
1442 .build();
1443
1444 let decoded: DynamoError =
1445 serde_json::from_str(&serde_json::to_string(&error).unwrap()).unwrap();
1446
1447 assert_eq!(decoded.error_type(), ErrorClass::CannotConnect);
1448 assert_eq!(decoded.class(), ErrorClass::Unavailable);
1449 assert_eq!(decoded.reason().as_str(), "transport.cannot_connect");
1450 }
1451
1452 #[test]
1453 fn worker_unavailable_roundtrips_with_semantic_identity() {
1454 let error = DynamoError::builder()
1455 .error_type(ErrorClass::WorkerUnavailable)
1456 .build();
1457
1458 let decoded: DynamoError =
1459 serde_json::from_str(&serde_json::to_string(&error).unwrap()).unwrap();
1460
1461 assert_eq!(decoded.error_type(), ErrorClass::WorkerUnavailable);
1462 assert_eq!(decoded.class(), ErrorClass::Unavailable);
1463 assert_eq!(decoded.reason().as_str(), "backend.worker_unavailable");
1464 }
1465
1466 #[test]
1467 fn response_timeout_subtypes_roundtrip() {
1468 for error_type in [
1469 ErrorClass::ResponseTimeout,
1470 ErrorClass::Backend(BackendError::ResponseTimeout),
1471 ] {
1472 let error = DynamoError::builder().error_type(error_type).build();
1473 let decoded: DynamoError =
1474 serde_json::from_str(&serde_json::to_string(&error).unwrap()).unwrap();
1475
1476 assert_eq!(decoded.error_type(), error_type);
1477 assert_eq!(decoded.class(), ErrorClass::DeadlineExceeded);
1478 }
1479 }
1480
1481 #[test]
1482 fn unknown_class_fails_closed_during_deserialization() {
1483 let json = r#"{
1484 "class":"FutureErrorClass",
1485 "reason":"runtime.internal",
1486 "public":{"type":"message","message":"must not escape"}
1487 }"#;
1488 let error: DynamoError = serde_json::from_str(json).unwrap();
1489
1490 assert_eq!(error.error_type(), ErrorClass::Internal);
1491 assert_eq!(error.class(), ErrorClass::Internal);
1492 assert_eq!(error.reason().as_str(), "runtime.invalid_error");
1493 assert!(error.public_details().is_none());
1494 }
1495
1496 #[test]
1497 fn error_class_deserialization_preserves_backend_and_fails_closed() {
1498 let backend: ErrorClass = serde_json::from_str(r#"{"Backend":"InvalidArgument"}"#).unwrap();
1499 assert_eq!(backend, ErrorClass::Backend(BackendError::InvalidArgument));
1500
1501 let unknown: ErrorClass = serde_json::from_str(r#""FutureErrorClass""#).unwrap();
1502 assert_eq!(unknown, ErrorClass::Unknown);
1503 }
1504
1505 #[test]
1506 fn wrapping_preserves_the_semantic_head_across_the_wire() {
1507 #[derive(Debug)]
1508 struct Wrapper(DynamoError);
1509
1510 impl fmt::Display for Wrapper {
1511 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1512 f.write_str("transport wrapper")
1513 }
1514 }
1515
1516 impl std::error::Error for Wrapper {
1517 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1518 Some(&self.0)
1519 }
1520 }
1521
1522 let wrapped = Wrapper(
1523 DynamoError::builder()
1524 .class(ErrorClass::Unavailable)
1525 .reason(ErrorReason::new("transport.disconnected").unwrap())
1526 .build(),
1527 );
1528 let error = DynamoError::from(&wrapped as &(dyn std::error::Error + 'static));
1529
1530 assert_eq!(error.class(), ErrorClass::Unavailable);
1531 assert_eq!(error.reason().as_str(), "transport.disconnected");
1532 let value = serde_json::to_value(&error).unwrap();
1533 assert!(value.get("caused_by").is_some());
1534 let decoded: DynamoError = serde_json::from_value(value).unwrap();
1535 assert_eq!(decoded.class(), ErrorClass::Unavailable);
1536 assert_eq!(decoded.reason().as_str(), "transport.disconnected");
1537 }
1538
1539 #[test]
1540 fn malformed_semantic_reason_fails_closed() {
1541 let json = r#"{
1542 "class": "InvalidRequest",
1543 "reason": "INVALID REASON",
1544 "diagnostic": "private details",
1545 "public": {"type": "rate_limit", "limit": 10, "remaining": 0}
1546 }"#;
1547 let err: DynamoError = serde_json::from_str(json).unwrap();
1548
1549 assert_eq!(err.error_type(), ErrorClass::Internal);
1550 assert_eq!(err.class(), ErrorClass::Internal);
1551 assert_eq!(err.reason().as_str(), "runtime.invalid_error");
1552 assert!(err.public_details().is_none());
1553 }
1554
1555 #[test]
1556 fn test_error_type_display() {
1557 assert_eq!(ErrorClass::Unknown.to_string(), "Unknown");
1558 assert_eq!(ErrorClass::InvalidArgument.to_string(), "InvalidArgument");
1559 assert_eq!(ErrorClass::CannotConnect.to_string(), "CannotConnect");
1560 assert_eq!(ErrorClass::Disconnected.to_string(), "Disconnected");
1561 assert_eq!(
1562 ErrorClass::ConnectionTimeout.to_string(),
1563 "ConnectionTimeout"
1564 );
1565 assert_eq!(ErrorClass::ResponseTimeout.to_string(), "ResponseTimeout");
1566 assert_eq!(ErrorClass::Cancelled.to_string(), "Cancelled");
1567 assert_eq!(ErrorClass::DeadlineExceeded.to_string(), "DeadlineExceeded");
1568 assert_eq!(
1569 ErrorClass::ResourceExhausted.to_string(),
1570 "ResourceExhausted"
1571 );
1572 assert_eq!(ErrorClass::WorkerOverloaded.to_string(), "WorkerOverloaded");
1573 assert_eq!(ErrorClass::Unavailable.to_string(), "Unavailable");
1574 assert_eq!(
1575 ErrorClass::WorkerUnavailable.to_string(),
1576 "WorkerUnavailable"
1577 );
1578 assert_eq!(
1579 ErrorClass::Backend(BackendError::Unknown).to_string(),
1580 "BackendUnknown"
1581 );
1582 assert_eq!(
1583 ErrorClass::Backend(BackendError::InvalidArgument).to_string(),
1584 "BackendInvalidArgument"
1585 );
1586 assert_eq!(
1587 ErrorClass::Backend(BackendError::CannotConnect).to_string(),
1588 "BackendCannotConnect"
1589 );
1590 assert_eq!(
1591 ErrorClass::Backend(BackendError::Disconnected).to_string(),
1592 "BackendDisconnected"
1593 );
1594 assert_eq!(
1595 ErrorClass::Backend(BackendError::ConnectionTimeout).to_string(),
1596 "BackendConnectionTimeout"
1597 );
1598 assert_eq!(
1599 ErrorClass::Backend(BackendError::Cancelled).to_string(),
1600 "BackendCancelled"
1601 );
1602 assert_eq!(
1603 ErrorClass::Backend(BackendError::EngineShutdown).to_string(),
1604 "BackendEngineShutdown"
1605 );
1606 assert_eq!(
1607 ErrorClass::Backend(BackendError::StreamIncomplete).to_string(),
1608 "BackendStreamIncomplete"
1609 );
1610 assert_eq!(
1611 ErrorClass::Backend(BackendError::ResponseTimeout).to_string(),
1612 "BackendResponseTimeout"
1613 );
1614 }
1615}