1#![forbid(unsafe_code)]
2
3mod replication;
11pub use replication::ReplicationStreamErrorV3;
12
13use std::error::Error;
14use std::fmt;
15
16use riffdb_types::{
17 ContractLineage, ContractVersion, ExecutionFailureCode, FieldId, IncidentId, RequestId,
18};
19
20pub const APPLICATION_ERROR_ENVELOPE_VERSION: u32 = 1;
22pub const MAX_APPLICATION_ERROR_BYTES: usize = 16 * 1024;
24pub const MAX_APPLICATION_SYMBOL_PATH_SEGMENTS: usize = 16;
26pub const MAX_APPLICATION_SYMBOL_BYTES: usize = 256;
28pub const MAX_APPLICATION_FIXES: usize = 8;
30pub const MAX_APPLICATION_SOURCE_OFFSET: u64 = 262_144;
32
33#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
35pub enum ApplicationOperation {
36 DescribeContract,
38 CheckQuery,
40 ExplainQuery,
42 ExecuteQuery,
44 DeployQueryModule,
46 DeployReactiveModule,
48 GetQueryModule,
50 ExecuteCommand,
52 BatchCommand,
54 ExecuteProjectedQuery,
56 InspectVectorState,
58}
59
60impl ApplicationOperation {
61 #[must_use]
63 pub const fn as_str(self) -> &'static str {
64 match self {
65 Self::DescribeContract => "DescribeContract",
66 Self::CheckQuery => "CheckQuery",
67 Self::ExplainQuery => "ExplainQuery",
68 Self::ExecuteQuery => "ExecuteQuery",
69 Self::DeployQueryModule => "DeployQueryModule",
70 Self::DeployReactiveModule => "DeployReactiveModule",
71 Self::GetQueryModule => "GetQueryModule",
72 Self::ExecuteCommand => "ExecuteCommand",
73 Self::BatchCommand => "BatchCommand",
74 Self::ExecuteProjectedQuery => "ExecuteProjectedQuery",
75 Self::InspectVectorState => "InspectVectorState",
76 }
77 }
78}
79
80#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
82pub enum ApplicationErrorCode {
83 InvalidRequest,
85 InputInvalid,
87 AuthorizationDenied,
89 ContractMismatch,
91 QueryInvalid,
93 QueryUnavailable,
95 ModuleUnavailable,
97 CursorInvalid,
99 ResponseTooLarge,
101 StorageUnavailable,
103 OutcomeUnknown,
105 OperationCancelled,
107 DeadlineExceeded,
109 InternalDefect,
111 IdempotencyKeyReuse,
113 CommandExecutionFailed,
115 CapabilityRevoked,
117 ProtocolInvalid,
119 HistoryIncarnationMismatch,
121 HistoryPruned,
123 Overloaded,
125 ProjectionDiverged,
127 SnapshotRetired,
129 FreshnessUnsatisfied,
131 ProjectedSourceRequired,
133 FollowerMode,
135 PrimaryFenced,
137}
138
139pub const APPLICATION_ERROR_CODES: [ApplicationErrorCode; 27] = [
141 ApplicationErrorCode::InvalidRequest,
142 ApplicationErrorCode::InputInvalid,
143 ApplicationErrorCode::AuthorizationDenied,
144 ApplicationErrorCode::ContractMismatch,
145 ApplicationErrorCode::QueryInvalid,
146 ApplicationErrorCode::QueryUnavailable,
147 ApplicationErrorCode::ModuleUnavailable,
148 ApplicationErrorCode::CursorInvalid,
149 ApplicationErrorCode::ResponseTooLarge,
150 ApplicationErrorCode::StorageUnavailable,
151 ApplicationErrorCode::OutcomeUnknown,
152 ApplicationErrorCode::OperationCancelled,
153 ApplicationErrorCode::DeadlineExceeded,
154 ApplicationErrorCode::InternalDefect,
155 ApplicationErrorCode::IdempotencyKeyReuse,
156 ApplicationErrorCode::CommandExecutionFailed,
157 ApplicationErrorCode::CapabilityRevoked,
158 ApplicationErrorCode::ProtocolInvalid,
159 ApplicationErrorCode::HistoryIncarnationMismatch,
160 ApplicationErrorCode::HistoryPruned,
161 ApplicationErrorCode::Overloaded,
162 ApplicationErrorCode::ProjectionDiverged,
163 ApplicationErrorCode::SnapshotRetired,
164 ApplicationErrorCode::FreshnessUnsatisfied,
165 ApplicationErrorCode::ProjectedSourceRequired,
166 ApplicationErrorCode::FollowerMode,
167 ApplicationErrorCode::PrimaryFenced,
168];
169
170impl ApplicationErrorCode {
171 #[must_use]
173 pub const fn from_public_kind(kind: PublicErrorKind) -> Self {
174 match kind {
175 PublicErrorKind::Validation => Self::InputInvalid,
176 PublicErrorKind::IdempotencyKeyReuse => Self::IdempotencyKeyReuse,
177 PublicErrorKind::AuthorizationDenied => Self::AuthorizationDenied,
178 PublicErrorKind::ConcurrencyDeadlineExceeded => Self::DeadlineExceeded,
179 PublicErrorKind::ContractMismatch => Self::ContractMismatch,
180 PublicErrorKind::StorageUnavailable => Self::StorageUnavailable,
181 PublicErrorKind::OutcomeUnknown => Self::OutcomeUnknown,
182 PublicErrorKind::InternalDefect => Self::InternalDefect,
183 PublicErrorKind::CommandExecutionFailed => Self::CommandExecutionFailed,
184 PublicErrorKind::HistoryIncarnationMismatch => Self::HistoryIncarnationMismatch,
185 PublicErrorKind::HistoryPruned => Self::HistoryPruned,
186 PublicErrorKind::Overloaded => Self::Overloaded,
187 PublicErrorKind::FollowerMode => Self::FollowerMode,
188 PublicErrorKind::PrimaryFenced => Self::PrimaryFenced,
189 }
190 }
191
192 #[must_use]
194 pub const fn as_str(self) -> &'static str {
195 match self {
196 Self::InvalidRequest => "RDB-APP-0001",
197 Self::InputInvalid => "RDB-INPUT-0101",
198 Self::AuthorizationDenied => "RDB-AUTH-0214",
199 Self::ContractMismatch => "RDB-CONTRACT-0101",
200 Self::QueryInvalid => "RDB-QUERY-0101",
201 Self::QueryUnavailable => "RDB-QUERY-0102",
202 Self::ModuleUnavailable => "RDB-MODULE-0101",
203 Self::CursorInvalid => "RDB-CURSOR-0101",
204 Self::ResponseTooLarge => "RDB-RESOURCE-0101",
205 Self::StorageUnavailable => "RDB-STORAGE-0101",
206 Self::OutcomeUnknown => "RDB-UNCERTAIN-0101",
207 Self::OperationCancelled => "RDB-APP-0002",
208 Self::DeadlineExceeded => "RDB-APP-0003",
209 Self::InternalDefect => "RDB-INTERNAL-0001",
210 Self::IdempotencyKeyReuse => "RDB-COMMAND-0101",
211 Self::CommandExecutionFailed => "RDB-COMMAND-0102",
212 Self::CapabilityRevoked => "RDB-AUTH-0215",
213 Self::ProtocolInvalid => "RDB-PROTOCOL-0101",
214 Self::HistoryIncarnationMismatch => "RDB-HISTORY-0101",
215 Self::HistoryPruned => "RDB-HISTORY-0102",
216 Self::Overloaded => "RDB-CAPACITY-0101",
217 Self::ProjectionDiverged => "RDB-PROJECTION-0101",
218 Self::SnapshotRetired => "RDB-PROJECTION-0102",
219 Self::FreshnessUnsatisfied => "RDB-PROJECTION-0103",
220 Self::ProjectedSourceRequired => "RDB-PROJECTION-0104",
221 Self::FollowerMode => "RDB-REP-0101",
222 Self::PrimaryFenced => "RDB-REP-0102",
223 }
224 }
225
226 #[must_use]
228 pub const fn safe_message(self) -> &'static str {
229 match self {
230 Self::InvalidRequest => "application request is structurally invalid",
231 Self::InputInvalid => "application input is invalid",
232 Self::AuthorizationDenied => "application operation is not authorized",
233 Self::ContractMismatch => "application contract does not match",
234 Self::QueryInvalid => "RiffQL query is invalid",
235 Self::QueryUnavailable => "RiffQL query is unavailable",
236 Self::ModuleUnavailable => "query module is unavailable",
237 Self::CursorInvalid => "query cursor is invalid or stale",
238 Self::ResponseTooLarge => "application result exceeds the service limit",
239 Self::StorageUnavailable => "storage is temporarily unavailable",
240 Self::OutcomeUnknown => "command outcome is not yet known",
241 Self::OperationCancelled => "application request was cancelled",
242 Self::DeadlineExceeded => "application request deadline elapsed",
243 Self::InternalDefect => "an internal error occurred",
244 Self::IdempotencyKeyReuse => {
245 "idempotency key was reused with different application input"
246 }
247 Self::CommandExecutionFailed => "command execution failed",
248 Self::CapabilityRevoked => "application capability is revoked",
249 Self::ProtocolInvalid => "the RiffDB peer returned an invalid application response",
250 Self::HistoryIncarnationMismatch => "observed history predates a database restore",
251 Self::HistoryPruned => "requested history has been pruned",
252 Self::Overloaded => "service is over capacity",
253 Self::FollowerMode => "operation is unavailable in follower mode",
254 Self::PrimaryFenced => "primary is durably fenced",
255 Self::ProjectionDiverged => "query projections cannot prove one common snapshot",
256 Self::SnapshotRetired => "the requested query snapshot has been retired",
257 Self::FreshnessUnsatisfied => "no query snapshot satisfies the requested freshness",
258 Self::ProjectedSourceRequired => {
259 "nearest query requires a compiler-owned projected source"
260 }
261 }
262 }
263
264 #[must_use]
266 pub const fn category(self) -> ApplicationErrorCategory {
267 match self {
268 Self::InvalidRequest | Self::InputInvalid => ApplicationErrorCategory::Input,
269 Self::AuthorizationDenied | Self::CapabilityRevoked => {
270 ApplicationErrorCategory::Authorization
271 }
272 Self::ContractMismatch => ApplicationErrorCategory::Contract,
273 Self::QueryInvalid
274 | Self::QueryUnavailable
275 | Self::ProjectionDiverged
276 | Self::SnapshotRetired
277 | Self::FreshnessUnsatisfied => ApplicationErrorCategory::Query,
278 Self::ProjectedSourceRequired => ApplicationErrorCategory::Query,
279 Self::ModuleUnavailable => ApplicationErrorCategory::Module,
280 Self::CursorInvalid => ApplicationErrorCategory::Cursor,
281 Self::ResponseTooLarge => ApplicationErrorCategory::Resource,
282 Self::StorageUnavailable => ApplicationErrorCategory::Storage,
283 Self::OutcomeUnknown => ApplicationErrorCategory::Uncertainty,
284 Self::OperationCancelled
285 | Self::DeadlineExceeded
286 | Self::FollowerMode
287 | Self::PrimaryFenced => ApplicationErrorCategory::Control,
288 Self::InternalDefect => ApplicationErrorCategory::Internal,
289 Self::IdempotencyKeyReuse | Self::CommandExecutionFailed => {
290 ApplicationErrorCategory::Command
291 }
292 Self::ProtocolInvalid => ApplicationErrorCategory::Protocol,
293 Self::HistoryIncarnationMismatch | Self::HistoryPruned => {
294 ApplicationErrorCategory::History
295 }
296 Self::Overloaded => ApplicationErrorCategory::Capacity,
297 }
298 }
299
300 #[must_use]
302 pub const fn recovery_action(self) -> ApplicationRecoveryAction {
303 match self {
304 Self::InvalidRequest
305 | Self::InputInvalid
306 | Self::QueryInvalid
307 | Self::CursorInvalid
308 | Self::ResponseTooLarge
309 | Self::IdempotencyKeyReuse
310 | Self::HistoryIncarnationMismatch
311 | Self::HistoryPruned
312 | Self::SnapshotRetired
313 | Self::FollowerMode => ApplicationRecoveryAction::CorrectRequest,
314 Self::ProjectedSourceRequired => ApplicationRecoveryAction::RefreshContract,
315 Self::AuthorizationDenied | Self::CapabilityRevoked => {
316 ApplicationRecoveryAction::ObtainPermission
317 }
318 Self::ContractMismatch | Self::QueryUnavailable | Self::ModuleUnavailable => {
319 ApplicationRecoveryAction::RefreshContract
320 }
321 Self::StorageUnavailable
322 | Self::DeadlineExceeded
323 | Self::Overloaded
324 | Self::ProjectionDiverged
325 | Self::FreshnessUnsatisfied => ApplicationRecoveryAction::Retry,
326 Self::OutcomeUnknown => ApplicationRecoveryAction::ResolveWithSameIdempotencyKey,
327 Self::OperationCancelled => ApplicationRecoveryAction::None,
328 Self::InternalDefect
329 | Self::CommandExecutionFailed
330 | Self::ProtocolInvalid
331 | Self::PrimaryFenced => ApplicationRecoveryAction::ContactOperator,
332 }
333 }
334
335 #[must_use]
337 pub const fn fixes(self) -> &'static [ApplicationFixCode] {
338 match self {
339 Self::InvalidRequest
340 | Self::InputInvalid
341 | Self::QueryInvalid
342 | Self::HistoryIncarnationMismatch
343 | Self::HistoryPruned => &[ApplicationFixCode::CorrectInput],
344 Self::AuthorizationDenied | Self::CapabilityRevoked => {
345 &[ApplicationFixCode::BindApplicationRole]
346 }
347 Self::ContractMismatch => &[ApplicationFixCode::RefreshContract],
348 Self::QueryUnavailable | Self::ModuleUnavailable => {
349 &[ApplicationFixCode::PinActiveModule]
350 }
351 Self::ProjectedSourceRequired => &[ApplicationFixCode::PinActiveModule],
352 Self::CursorInvalid => &[ApplicationFixCode::RestartFromFirstPage],
353 Self::SnapshotRetired => &[ApplicationFixCode::RestartFromFirstPage],
354 Self::ResponseTooLarge | Self::IdempotencyKeyReuse => {
355 &[ApplicationFixCode::CorrectInput]
356 }
357 Self::StorageUnavailable
358 | Self::DeadlineExceeded
359 | Self::Overloaded
360 | Self::ProjectionDiverged
361 | Self::FreshnessUnsatisfied => &[ApplicationFixCode::RetryLater],
362 Self::OutcomeUnknown => &[ApplicationFixCode::ResolveWithSameIdempotencyKey],
363 Self::InternalDefect => &[ApplicationFixCode::ContactOperatorWithIncident],
364 Self::OperationCancelled
365 | Self::CommandExecutionFailed
366 | Self::ProtocolInvalid
367 | Self::FollowerMode
368 | Self::PrimaryFenced => &[],
369 }
370 }
371}
372
373#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
375pub enum ApplicationErrorCategory {
376 Input,
378 Authorization,
380 Contract,
382 Query,
384 Module,
386 Cursor,
388 Resource,
390 Storage,
392 Uncertainty,
394 Internal,
396 Protocol,
398 Command,
400 Control,
402 History,
404 Capacity,
406}
407
408impl ApplicationErrorCategory {
409 #[must_use]
411 pub const fn as_str(self) -> &'static str {
412 match self {
413 Self::Input => "input",
414 Self::Authorization => "authorization",
415 Self::Contract => "contract",
416 Self::Query => "query",
417 Self::Module => "module",
418 Self::Cursor => "cursor",
419 Self::Resource => "resource",
420 Self::Storage => "storage",
421 Self::Uncertainty => "uncertainty",
422 Self::Internal => "internal",
423 Self::Protocol => "protocol",
424 Self::Command => "command",
425 Self::Control => "control",
426 Self::History => "history",
427 Self::Capacity => "capacity",
428 }
429 }
430}
431
432#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
434pub enum ApplicationRecoveryAction {
435 CorrectRequest,
437 Retry,
439 ResolveWithSameIdempotencyKey,
441 ObtainPermission,
443 RefreshContract,
445 ContactOperator,
447 None,
449}
450
451impl ApplicationRecoveryAction {
452 #[must_use]
454 pub const fn as_str(self) -> &'static str {
455 match self {
456 Self::CorrectRequest => "correct_request",
457 Self::Retry => "retry",
458 Self::ResolveWithSameIdempotencyKey => "resolve_with_same_idempotency_key",
459 Self::ObtainPermission => "obtain_permission",
460 Self::RefreshContract => "refresh_contract",
461 Self::ContactOperator => "contact_operator",
462 Self::None => "none",
463 }
464 }
465}
466
467#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
469pub enum ApplicationFixCode {
470 CorrectInput,
472 RemoveForbiddenOutput,
474 BindApplicationRole,
476 RefreshContract,
478 PinActiveModule,
480 AddBoundedIndex,
482 RestartFromFirstPage,
484 RetryLater,
486 ResolveWithSameIdempotencyKey,
488 ContactOperatorWithIncident,
490}
491
492impl ApplicationFixCode {
493 #[must_use]
495 pub const fn as_str(self) -> &'static str {
496 match self {
497 Self::CorrectInput => "correct_input",
498 Self::RemoveForbiddenOutput => "remove_forbidden_output",
499 Self::BindApplicationRole => "bind_application_role",
500 Self::RefreshContract => "refresh_contract",
501 Self::PinActiveModule => "pin_active_module",
502 Self::AddBoundedIndex => "add_bounded_index",
503 Self::RestartFromFirstPage => "restart_from_first_page",
504 Self::RetryLater => "retry_later",
505 Self::ResolveWithSameIdempotencyKey => "resolve_with_same_idempotency_key",
506 Self::ContactOperatorWithIncident => "contact_operator_with_incident",
507 }
508 }
509}
510
511#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
513pub struct ApplicationSourceSpan {
514 start: u64,
515 end: u64,
516}
517
518impl ApplicationSourceSpan {
519 pub const fn new(start: u64, end: u64) -> Option<Self> {
521 if start <= end && end <= MAX_APPLICATION_SOURCE_OFFSET {
522 Some(Self { start, end })
523 } else {
524 None
525 }
526 }
527
528 #[must_use]
530 pub const fn start(self) -> u64 {
531 self.start
532 }
533
534 #[must_use]
536 pub const fn end(self) -> u64 {
537 self.end
538 }
539}
540
541#[derive(Clone, Debug, Default, Eq, PartialEq)]
543pub struct ApplicationErrorContext {
544 contract: Option<(ContractLineage, ContractVersion)>,
545 operation_symbol: Option<String>,
546 symbol_path: Vec<String>,
547 source_span: Option<ApplicationSourceSpan>,
548 trace_id: Option<RequestId>,
549}
550
551impl ApplicationErrorContext {
552 #[must_use]
554 pub const fn empty() -> Self {
555 Self {
556 contract: None,
557 operation_symbol: None,
558 symbol_path: Vec::new(),
559 source_span: None,
560 trace_id: None,
561 }
562 }
563
564 #[must_use]
566 pub fn with_contract(mut self, lineage: ContractLineage, version: ContractVersion) -> Self {
567 self.contract = Some((lineage, version));
568 self
569 }
570
571 pub fn with_operation_symbol(
573 mut self,
574 symbol: String,
575 ) -> Result<Self, ApplicationErrorContextError> {
576 validate_application_symbol(&symbol)?;
577 self.operation_symbol = Some(symbol);
578 Ok(self)
579 }
580
581 pub fn with_symbol_path(
583 mut self,
584 symbols: Vec<String>,
585 ) -> Result<Self, ApplicationErrorContextError> {
586 if symbols.len() > MAX_APPLICATION_SYMBOL_PATH_SEGMENTS {
587 return Err(ApplicationErrorContextError);
588 }
589 for symbol in &symbols {
590 validate_application_symbol(symbol)?;
591 }
592 self.symbol_path = symbols;
593 Ok(self)
594 }
595
596 #[must_use]
598 pub const fn with_source_span(mut self, source_span: ApplicationSourceSpan) -> Self {
599 self.source_span = Some(source_span);
600 self
601 }
602
603 #[must_use]
605 pub const fn with_trace_id(mut self, trace_id: RequestId) -> Self {
606 self.trace_id = Some(trace_id);
607 self
608 }
609
610 #[must_use]
612 pub const fn contract(&self) -> Option<&(ContractLineage, ContractVersion)> {
613 self.contract.as_ref()
614 }
615
616 #[must_use]
618 pub fn operation_symbol(&self) -> Option<&str> {
619 self.operation_symbol.as_deref()
620 }
621
622 #[must_use]
624 pub fn symbol_path(&self) -> &[String] {
625 &self.symbol_path
626 }
627
628 #[must_use]
630 pub const fn source_span(&self) -> Option<ApplicationSourceSpan> {
631 self.source_span
632 }
633
634 #[must_use]
636 pub const fn trace_id(&self) -> Option<RequestId> {
637 self.trace_id
638 }
639}
640
641fn validate_application_symbol(symbol: &str) -> Result<(), ApplicationErrorContextError> {
642 if symbol.is_empty()
643 || symbol.len() > MAX_APPLICATION_SYMBOL_BYTES
644 || !symbol
645 .bytes()
646 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
647 {
648 return Err(ApplicationErrorContextError);
649 }
650 Ok(())
651}
652
653#[derive(Clone, Copy, Debug, Eq, PartialEq)]
655pub struct ApplicationErrorContextError;
656
657impl fmt::Display for ApplicationErrorContextError {
658 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
659 formatter.write_str("application error context is invalid")
660 }
661}
662
663impl Error for ApplicationErrorContextError {}
664
665#[derive(Clone, Debug, Eq, PartialEq)]
667pub struct ApplicationError {
668 code: ApplicationErrorCode,
669 operation: ApplicationOperation,
670 context: ApplicationErrorContext,
671 incident_id: Option<IncidentId>,
672}
673
674impl ApplicationError {
675 #[must_use]
677 pub const fn new(
678 code: ApplicationErrorCode,
679 operation: ApplicationOperation,
680 context: ApplicationErrorContext,
681 incident_id: Option<IncidentId>,
682 ) -> Self {
683 Self {
684 code,
685 operation,
686 context,
687 incident_id,
688 }
689 }
690
691 #[must_use]
696 pub fn from_public_error(
697 error: &PublicError,
698 operation: ApplicationOperation,
699 context: ApplicationErrorContext,
700 ) -> Self {
701 Self::new(
702 error
703 .application_code_hint()
704 .unwrap_or_else(|| ApplicationErrorCode::from_public_kind(error.kind())),
705 operation,
706 context,
707 error.incident_id().copied(),
708 )
709 }
710
711 #[must_use]
713 pub const fn code(&self) -> ApplicationErrorCode {
714 self.code
715 }
716
717 #[must_use]
719 pub const fn operation(&self) -> ApplicationOperation {
720 self.operation
721 }
722
723 #[must_use]
725 pub const fn context(&self) -> &ApplicationErrorContext {
726 &self.context
727 }
728
729 #[must_use]
731 pub const fn incident_id(&self) -> Option<&IncidentId> {
732 self.incident_id.as_ref()
733 }
734
735 #[must_use]
737 pub const fn category(&self) -> ApplicationErrorCategory {
738 self.code.category()
739 }
740
741 #[must_use]
743 pub const fn safe_message(&self) -> &'static str {
744 self.code.safe_message()
745 }
746
747 #[must_use]
749 pub const fn recovery_action(&self) -> ApplicationRecoveryAction {
750 self.code.recovery_action()
751 }
752
753 #[must_use]
755 pub const fn fixes(&self) -> &'static [ApplicationFixCode] {
756 self.code.fixes()
757 }
758}
759
760impl fmt::Display for ApplicationError {
761 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
762 write!(
763 formatter,
764 "{}: {} [{}]",
765 self.code.as_str(),
766 self.safe_message(),
767 self.operation.as_str()
768 )?;
769 if let Some(symbol) = self.context.operation_symbol() {
770 write!(formatter, " {symbol}")?;
771 }
772 if let Some(incident_id) = &self.incident_id {
773 write!(formatter, " (incident {incident_id})")?;
774 }
775 Ok(())
776 }
777}
778
779impl Error for ApplicationError {}
780
781pub trait IncidentIdSource: Send + Sync {
787 fn next_incident_id(&self) -> Result<IncidentId, IncidentIdSourceError>;
789}
790
791#[derive(Clone, Copy, Debug, Eq, PartialEq)]
793pub struct IncidentIdSourceError;
794
795impl fmt::Display for IncidentIdSourceError {
796 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
797 formatter.write_str("incident identifier source failed")
798 }
799}
800
801impl Error for IncidentIdSourceError {}
802
803pub const MAX_VALIDATION_ISSUES: usize = 16;
805
806pub const MAX_VALIDATION_PATH_SEGMENTS: usize = 16;
808
809#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
811pub enum ErrorClass {
812 InvalidArgument,
814 Conflict,
816 PermissionDenied,
818 DeadlineExceeded,
820 FailedPrecondition,
822 Unavailable,
824 Uncertain,
826 Internal,
828}
829
830#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
832pub enum RecoveryAction {
833 CorrectRequest,
835 Retry,
837 ResolveWithSameIdempotencyKey,
839 ObtainPermission,
841 RefreshContract,
843 ContactOperator,
845}
846
847#[derive(Clone, Copy, Eq, PartialEq)]
860pub struct EmergencyInternalFailure {
861 _private: (),
862}
863
864impl EmergencyInternalFailure {
865 #[must_use]
867 pub const fn safe_message(self) -> &'static str {
868 "an internal error occurred"
869 }
870
871 #[must_use]
873 pub const fn class(self) -> ErrorClass {
874 ErrorClass::Internal
875 }
876
877 #[must_use]
879 pub const fn recovery_action(self) -> RecoveryAction {
880 RecoveryAction::ContactOperator
881 }
882}
883
884impl From<IncidentIdSourceError> for EmergencyInternalFailure {
885 fn from(_: IncidentIdSourceError) -> Self {
886 Self { _private: () }
887 }
888}
889
890impl fmt::Display for EmergencyInternalFailure {
891 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
892 formatter.write_str(self.safe_message())
893 }
894}
895
896impl fmt::Debug for EmergencyInternalFailure {
897 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
898 formatter.write_str("EmergencyInternalFailure")
899 }
900}
901
902impl Error for EmergencyInternalFailure {}
903
904#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
906pub enum PublicErrorKind {
907 Validation,
909 IdempotencyKeyReuse,
911 AuthorizationDenied,
913 ConcurrencyDeadlineExceeded,
916 ContractMismatch,
918 StorageUnavailable,
920 OutcomeUnknown,
922 InternalDefect,
924 CommandExecutionFailed,
926 HistoryIncarnationMismatch,
928 HistoryPruned,
930 Overloaded,
932 FollowerMode,
934 PrimaryFenced,
936}
937
938impl PublicErrorKind {
939 #[must_use]
941 pub const fn code(self) -> &'static str {
942 match self {
943 Self::Validation => "validation_failed",
944 Self::IdempotencyKeyReuse => "idempotency_key_reuse",
945 Self::AuthorizationDenied => "authorization_denied",
946 Self::ConcurrencyDeadlineExceeded => "concurrency_deadline_exceeded",
947 Self::ContractMismatch => "contract_mismatch",
948 Self::StorageUnavailable => "storage_unavailable",
949 Self::OutcomeUnknown => "outcome_unknown",
950 Self::InternalDefect => "internal_defect",
951 Self::CommandExecutionFailed => "command_execution_failed",
952 Self::HistoryIncarnationMismatch => "history_incarnation_mismatch",
953 Self::HistoryPruned => "history_pruned",
954 Self::Overloaded => "overloaded",
955 Self::FollowerMode => "follower_mode",
956 Self::PrimaryFenced => "primary_fenced",
957 }
958 }
959
960 #[must_use]
962 pub const fn safe_message(self) -> &'static str {
963 match self {
964 Self::Validation => "request validation failed",
965 Self::IdempotencyKeyReuse => "idempotency key was reused with different input",
966 Self::AuthorizationDenied => "operation is not authorized",
967 Self::ConcurrencyDeadlineExceeded => "concurrency deadline exceeded",
968 Self::ContractMismatch => "contract version or plan does not match",
969 Self::StorageUnavailable => "storage is temporarily unavailable",
970 Self::OutcomeUnknown => "command outcome is not yet known",
971 Self::InternalDefect => "an internal error occurred",
972 Self::CommandExecutionFailed => "command execution failed",
973 Self::HistoryIncarnationMismatch => "observed history predates a database restore",
974 Self::HistoryPruned => "requested history has been pruned",
975 Self::Overloaded => "service is over capacity",
976 Self::FollowerMode => "operation is unavailable in follower mode",
977 Self::PrimaryFenced => "primary is durably fenced",
978 }
979 }
980
981 #[must_use]
983 pub const fn class(self) -> ErrorClass {
984 match self {
985 Self::Validation => ErrorClass::InvalidArgument,
986 Self::IdempotencyKeyReuse => ErrorClass::Conflict,
987 Self::AuthorizationDenied => ErrorClass::PermissionDenied,
988 Self::ConcurrencyDeadlineExceeded => ErrorClass::DeadlineExceeded,
989 Self::ContractMismatch
990 | Self::HistoryIncarnationMismatch
991 | Self::HistoryPruned
992 | Self::FollowerMode
993 | Self::PrimaryFenced => ErrorClass::FailedPrecondition,
994 Self::StorageUnavailable | Self::Overloaded => ErrorClass::Unavailable,
995 Self::OutcomeUnknown => ErrorClass::Uncertain,
996 Self::InternalDefect => ErrorClass::Internal,
997 Self::CommandExecutionFailed => ErrorClass::FailedPrecondition,
998 }
999 }
1000
1001 #[must_use]
1011 pub const fn status_code(self) -> PublicErrorStatusCode {
1012 match self {
1013 Self::Validation => PublicErrorStatusCode::InvalidArgument,
1014 Self::IdempotencyKeyReuse => PublicErrorStatusCode::AlreadyExists,
1015 Self::AuthorizationDenied => PublicErrorStatusCode::PermissionDenied,
1016 Self::ConcurrencyDeadlineExceeded => PublicErrorStatusCode::DeadlineExceeded,
1017 Self::ContractMismatch
1018 | Self::CommandExecutionFailed
1019 | Self::HistoryIncarnationMismatch
1020 | Self::HistoryPruned
1021 | Self::FollowerMode
1022 | Self::PrimaryFenced => PublicErrorStatusCode::FailedPrecondition,
1023 Self::StorageUnavailable => PublicErrorStatusCode::Unavailable,
1024 Self::Overloaded => PublicErrorStatusCode::ResourceExhausted,
1025 Self::OutcomeUnknown => PublicErrorStatusCode::Unknown,
1026 Self::InternalDefect => PublicErrorStatusCode::Internal,
1027 }
1028 }
1029
1030 #[must_use]
1032 pub const fn recovery_action(self) -> RecoveryAction {
1033 match self {
1034 Self::Validation
1035 | Self::IdempotencyKeyReuse
1036 | Self::HistoryIncarnationMismatch
1037 | Self::HistoryPruned
1038 | Self::FollowerMode => RecoveryAction::CorrectRequest,
1039 Self::AuthorizationDenied => RecoveryAction::ObtainPermission,
1040 Self::ConcurrencyDeadlineExceeded | Self::StorageUnavailable | Self::Overloaded => {
1041 RecoveryAction::Retry
1042 }
1043 Self::ContractMismatch => RecoveryAction::RefreshContract,
1044 Self::OutcomeUnknown => RecoveryAction::ResolveWithSameIdempotencyKey,
1045 Self::InternalDefect | Self::CommandExecutionFailed | Self::PrimaryFenced => {
1046 RecoveryAction::ContactOperator
1047 }
1048 }
1049 }
1050}
1051
1052#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1059pub enum PublicErrorStatusCode {
1060 InvalidArgument,
1062 AlreadyExists,
1064 PermissionDenied,
1066 DeadlineExceeded,
1068 FailedPrecondition,
1070 ResourceExhausted,
1072 Unavailable,
1074 Unknown,
1076 Internal,
1078}
1079
1080#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1082pub enum ValidationCode {
1083 MissingRequiredValue,
1085 TypeMismatch,
1087 InvalidValue,
1089 OutOfRange,
1091 TooLong,
1093 TooManyItems,
1095 UnknownField,
1097 DuplicateField,
1099}
1100
1101impl ValidationCode {
1102 #[must_use]
1104 pub const fn code(self) -> &'static str {
1105 match self {
1106 Self::MissingRequiredValue => "missing_required_value",
1107 Self::TypeMismatch => "type_mismatch",
1108 Self::InvalidValue => "invalid_value",
1109 Self::OutOfRange => "out_of_range",
1110 Self::TooLong => "too_long",
1111 Self::TooManyItems => "too_many_items",
1112 Self::UnknownField => "unknown_field",
1113 Self::DuplicateField => "duplicate_field",
1114 }
1115 }
1116
1117 #[must_use]
1119 pub const fn safe_message(self) -> &'static str {
1120 match self {
1121 Self::MissingRequiredValue => "required value is missing",
1122 Self::TypeMismatch => "value has the wrong type",
1123 Self::InvalidValue => "value is invalid",
1124 Self::OutOfRange => "value is outside the allowed range",
1125 Self::TooLong => "value exceeds the allowed length",
1126 Self::TooManyItems => "collection has too many items",
1127 Self::UnknownField => "field is not defined by the active contract",
1128 Self::DuplicateField => "field occurs more than once",
1129 }
1130 }
1131}
1132
1133#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1135pub enum ValidationPathSegment {
1136 Field(FieldId),
1138 ListIndex(u32),
1140}
1141
1142#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
1144pub struct ValidationPath {
1145 segments: Vec<ValidationPathSegment>,
1146}
1147
1148impl ValidationPath {
1149 #[must_use]
1151 pub const fn root() -> Self {
1152 Self {
1153 segments: Vec::new(),
1154 }
1155 }
1156
1157 pub fn new(segments: Vec<ValidationPathSegment>) -> Result<Self, ValidationPathError> {
1159 if segments.len() > MAX_VALIDATION_PATH_SEGMENTS {
1160 return Err(ValidationPathError::TooManySegments);
1161 }
1162 Ok(Self { segments })
1163 }
1164
1165 #[must_use]
1167 pub fn segments(&self) -> &[ValidationPathSegment] {
1168 &self.segments
1169 }
1170}
1171
1172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1174pub enum ValidationPathError {
1175 TooManySegments,
1177}
1178
1179impl fmt::Display for ValidationPathError {
1180 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1181 formatter.write_str("validation path has too many segments")
1182 }
1183}
1184
1185impl Error for ValidationPathError {}
1186
1187#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1189pub struct ValidationIssue {
1190 code: ValidationCode,
1191 path: ValidationPath,
1192}
1193
1194impl ValidationIssue {
1195 #[must_use]
1197 pub const fn new(code: ValidationCode, path: ValidationPath) -> Self {
1198 Self { code, path }
1199 }
1200
1201 #[must_use]
1203 pub const fn code(&self) -> ValidationCode {
1204 self.code
1205 }
1206
1207 #[must_use]
1209 pub const fn path(&self) -> &ValidationPath {
1210 &self.path
1211 }
1212}
1213
1214#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1216pub struct ValidationIssues {
1217 issues: Vec<ValidationIssue>,
1218}
1219
1220impl ValidationIssues {
1221 pub fn new(issues: Vec<ValidationIssue>) -> Result<Self, ValidationIssuesError> {
1223 if issues.is_empty() {
1224 return Err(ValidationIssuesError::Empty);
1225 }
1226 if issues.len() > MAX_VALIDATION_ISSUES {
1227 return Err(ValidationIssuesError::TooManyIssues);
1228 }
1229 Ok(Self { issues })
1230 }
1231
1232 #[must_use]
1234 pub fn one(issue: ValidationIssue) -> Self {
1235 Self {
1236 issues: vec![issue],
1237 }
1238 }
1239
1240 #[must_use]
1242 pub fn as_slice(&self) -> &[ValidationIssue] {
1243 &self.issues
1244 }
1245}
1246
1247#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1249pub enum ValidationIssuesError {
1250 Empty,
1252 TooManyIssues,
1254}
1255
1256impl fmt::Display for ValidationIssuesError {
1257 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1258 formatter.write_str(match self {
1259 Self::Empty => "validation issue collection is empty",
1260 Self::TooManyIssues => "validation issue collection has too many entries",
1261 })
1262 }
1263}
1264
1265impl Error for ValidationIssuesError {}
1266
1267#[derive(Clone, Debug, Eq, PartialEq)]
1269pub enum PublicErrorDetails {
1270 None,
1272 Validation(ValidationIssues),
1274 ContractMismatch {
1276 active_contract_version: ContractVersion,
1278 },
1279 CommandExecutionFailed {
1281 code: ExecutionFailureCode,
1283 },
1284}
1285
1286#[derive(Clone, Debug, Eq, PartialEq)]
1293pub struct PublicError {
1294 kind: PublicErrorKind,
1295 details: PublicErrorDetails,
1296 incident_id: Option<IncidentId>,
1297 application_code_hint: Option<ApplicationErrorCode>,
1298}
1299
1300impl PublicError {
1301 const fn contextless(kind: PublicErrorKind) -> Self {
1302 Self {
1303 kind,
1304 details: PublicErrorDetails::None,
1305 incident_id: None,
1306 application_code_hint: None,
1307 }
1308 }
1309
1310 #[must_use]
1312 pub const fn validation(issues: ValidationIssues) -> Self {
1313 Self {
1314 kind: PublicErrorKind::Validation,
1315 details: PublicErrorDetails::Validation(issues),
1316 incident_id: None,
1317 application_code_hint: None,
1318 }
1319 }
1320
1321 #[must_use]
1323 pub const fn idempotency_key_reuse() -> Self {
1324 Self::contextless(PublicErrorKind::IdempotencyKeyReuse)
1325 }
1326
1327 #[must_use]
1329 pub const fn authorization_denied() -> Self {
1330 Self::contextless(PublicErrorKind::AuthorizationDenied)
1331 }
1332
1333 #[must_use]
1336 pub const fn concurrency_deadline_exceeded() -> Self {
1337 Self::contextless(PublicErrorKind::ConcurrencyDeadlineExceeded)
1338 }
1339
1340 #[must_use]
1342 pub const fn contract_mismatch(active_contract_version: ContractVersion) -> Self {
1343 Self {
1344 kind: PublicErrorKind::ContractMismatch,
1345 details: PublicErrorDetails::ContractMismatch {
1346 active_contract_version,
1347 },
1348 incident_id: None,
1349 application_code_hint: None,
1350 }
1351 }
1352
1353 #[must_use]
1355 pub const fn storage_unavailable() -> Self {
1356 Self::contextless(PublicErrorKind::StorageUnavailable)
1357 }
1358
1359 #[must_use]
1361 pub const fn outcome_unknown() -> Self {
1362 Self::contextless(PublicErrorKind::OutcomeUnknown)
1363 }
1364
1365 #[must_use]
1367 pub const fn command_execution_failed(code: ExecutionFailureCode) -> Self {
1368 Self {
1369 kind: PublicErrorKind::CommandExecutionFailed,
1370 details: PublicErrorDetails::CommandExecutionFailed { code },
1371 incident_id: None,
1372 application_code_hint: None,
1373 }
1374 }
1375
1376 #[must_use]
1378 pub const fn internal_defect(incident_id: IncidentId) -> Self {
1379 Self {
1380 kind: PublicErrorKind::InternalDefect,
1381 details: PublicErrorDetails::None,
1382 incident_id: Some(incident_id),
1383 application_code_hint: None,
1384 }
1385 }
1386
1387 #[must_use]
1389 pub const fn history_incarnation_mismatch() -> Self {
1390 Self::contextless(PublicErrorKind::HistoryIncarnationMismatch)
1391 }
1392
1393 #[must_use]
1395 pub const fn history_pruned() -> Self {
1396 Self::contextless(PublicErrorKind::HistoryPruned)
1397 }
1398
1399 #[must_use]
1401 pub const fn follower_mode() -> Self {
1402 Self::contextless(PublicErrorKind::FollowerMode)
1403 }
1404
1405 #[must_use]
1408 pub const fn primary_fenced() -> Self {
1409 Self::contextless(PublicErrorKind::PrimaryFenced)
1410 }
1411
1412 #[must_use]
1414 pub const fn overloaded() -> Self {
1415 Self::contextless(PublicErrorKind::Overloaded)
1416 }
1417
1418 #[must_use]
1420 pub const fn with_incident_id(mut self, incident_id: IncidentId) -> Self {
1421 self.incident_id = Some(incident_id);
1422 self
1423 }
1424
1425 pub fn with_application_code_hint(
1429 mut self,
1430 code: ApplicationErrorCode,
1431 ) -> Result<Self, ApplicationErrorHintError> {
1432 let compatible = match self.kind {
1433 PublicErrorKind::Validation => matches!(
1434 code,
1435 ApplicationErrorCode::InvalidRequest
1436 | ApplicationErrorCode::InputInvalid
1437 | ApplicationErrorCode::QueryInvalid
1438 | ApplicationErrorCode::QueryUnavailable
1439 | ApplicationErrorCode::ModuleUnavailable
1440 | ApplicationErrorCode::CursorInvalid
1441 | ApplicationErrorCode::ProjectionDiverged
1442 | ApplicationErrorCode::SnapshotRetired
1443 | ApplicationErrorCode::FreshnessUnsatisfied
1444 | ApplicationErrorCode::ProjectedSourceRequired
1445 ),
1446 PublicErrorKind::AuthorizationDenied => matches!(
1447 code,
1448 ApplicationErrorCode::AuthorizationDenied | ApplicationErrorCode::CapabilityRevoked
1449 ),
1450 PublicErrorKind::ContractMismatch => code == ApplicationErrorCode::ContractMismatch,
1451 PublicErrorKind::StorageUnavailable => code == ApplicationErrorCode::StorageUnavailable,
1452 PublicErrorKind::OutcomeUnknown => code == ApplicationErrorCode::OutcomeUnknown,
1453 PublicErrorKind::InternalDefect => code == ApplicationErrorCode::InternalDefect,
1454 PublicErrorKind::IdempotencyKeyReuse => {
1455 code == ApplicationErrorCode::IdempotencyKeyReuse
1456 }
1457 PublicErrorKind::ConcurrencyDeadlineExceeded => {
1458 code == ApplicationErrorCode::DeadlineExceeded
1459 }
1460 PublicErrorKind::CommandExecutionFailed => {
1461 code == ApplicationErrorCode::CommandExecutionFailed
1462 }
1463 PublicErrorKind::HistoryIncarnationMismatch => {
1464 code == ApplicationErrorCode::HistoryIncarnationMismatch
1465 }
1466 PublicErrorKind::HistoryPruned => code == ApplicationErrorCode::HistoryPruned,
1467 PublicErrorKind::Overloaded => code == ApplicationErrorCode::Overloaded,
1468 PublicErrorKind::FollowerMode => code == ApplicationErrorCode::FollowerMode,
1469 PublicErrorKind::PrimaryFenced => code == ApplicationErrorCode::PrimaryFenced,
1470 };
1471 if !compatible {
1472 return Err(ApplicationErrorHintError);
1473 }
1474 self.application_code_hint = Some(code);
1475 Ok(self)
1476 }
1477
1478 #[must_use]
1480 pub const fn application_code_hint(&self) -> Option<ApplicationErrorCode> {
1481 self.application_code_hint
1482 }
1483
1484 #[must_use]
1486 pub const fn kind(&self) -> PublicErrorKind {
1487 self.kind
1488 }
1489
1490 #[must_use]
1492 pub const fn code(&self) -> &'static str {
1493 self.kind.code()
1494 }
1495
1496 #[must_use]
1498 pub const fn safe_message(&self) -> &'static str {
1499 self.kind.safe_message()
1500 }
1501
1502 #[must_use]
1504 pub const fn class(&self) -> ErrorClass {
1505 self.kind.class()
1506 }
1507
1508 #[must_use]
1510 pub const fn status_code(&self) -> PublicErrorStatusCode {
1511 self.kind.status_code()
1512 }
1513
1514 #[must_use]
1516 pub const fn recovery_action(&self) -> RecoveryAction {
1517 self.kind.recovery_action()
1518 }
1519
1520 #[must_use]
1522 pub const fn details(&self) -> &PublicErrorDetails {
1523 &self.details
1524 }
1525
1526 #[must_use]
1528 pub const fn incident_id(&self) -> Option<&IncidentId> {
1529 self.incident_id.as_ref()
1530 }
1531}
1532
1533#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1535pub struct ApplicationErrorHintError;
1536
1537impl fmt::Display for ApplicationErrorHintError {
1538 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1539 formatter.write_str("application error hint is incompatible")
1540 }
1541}
1542
1543impl Error for ApplicationErrorHintError {}
1544
1545impl fmt::Display for PublicError {
1546 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1547 write!(formatter, "{}: {}", self.code(), self.safe_message())?;
1548 if let Some(incident_id) = &self.incident_id {
1549 write!(formatter, " (incident {incident_id})")?;
1550 }
1551 Ok(())
1552 }
1553}
1554
1555impl Error for PublicError {}
1556
1557#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1565pub enum DefectScope {
1566 Process,
1570 Request,
1573}
1574
1575pub struct InternalError {
1581 incident_id: IncidentId,
1582 scope: DefectScope,
1583 source: Box<dyn Error + Send + Sync + 'static>,
1584}
1585
1586impl InternalError {
1587 pub fn new(
1593 incident_id: IncidentId,
1594 scope: DefectScope,
1595 source: impl Error + Send + Sync + 'static,
1596 ) -> Self {
1597 Self {
1598 incident_id,
1599 scope,
1600 source: Box::new(source),
1601 }
1602 }
1603
1604 #[must_use]
1606 pub const fn scope(&self) -> DefectScope {
1607 self.scope
1608 }
1609
1610 #[must_use]
1612 pub const fn incident_id(&self) -> &IncidentId {
1613 &self.incident_id
1614 }
1615
1616 #[must_use]
1618 pub fn into_public(self) -> PublicError {
1619 PublicError::internal_defect(self.incident_id)
1620 }
1621}
1622
1623impl fmt::Display for InternalError {
1624 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1625 write!(formatter, "internal defect (incident {})", self.incident_id)
1626 }
1627}
1628
1629impl fmt::Debug for InternalError {
1630 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1631 formatter
1632 .debug_struct("InternalError")
1633 .field("incident_id", &self.incident_id)
1634 .field("source", &"[REDACTED]")
1635 .finish()
1636 }
1637}
1638
1639impl Error for InternalError {
1640 fn source(&self) -> Option<&(dyn Error + 'static)> {
1641 Some(self.source.as_ref())
1642 }
1643}
1644
1645impl From<InternalError> for PublicError {
1646 fn from(error: InternalError) -> Self {
1647 error.into_public()
1648 }
1649}
1650
1651#[cfg(test)]
1652mod tests {
1653 use std::fmt::Write as _;
1654
1655 use super::*;
1656
1657 const KINDS: [PublicErrorKind; 14] = [
1658 PublicErrorKind::Validation,
1659 PublicErrorKind::IdempotencyKeyReuse,
1660 PublicErrorKind::AuthorizationDenied,
1661 PublicErrorKind::ConcurrencyDeadlineExceeded,
1662 PublicErrorKind::ContractMismatch,
1663 PublicErrorKind::StorageUnavailable,
1664 PublicErrorKind::OutcomeUnknown,
1665 PublicErrorKind::InternalDefect,
1666 PublicErrorKind::CommandExecutionFailed,
1667 PublicErrorKind::HistoryIncarnationMismatch,
1668 PublicErrorKind::HistoryPruned,
1669 PublicErrorKind::Overloaded,
1670 PublicErrorKind::FollowerMode,
1671 PublicErrorKind::PrimaryFenced,
1672 ];
1673
1674 const VALIDATION_CODES: [ValidationCode; 8] = [
1675 ValidationCode::MissingRequiredValue,
1676 ValidationCode::TypeMismatch,
1677 ValidationCode::InvalidValue,
1678 ValidationCode::OutOfRange,
1679 ValidationCode::TooLong,
1680 ValidationCode::TooManyItems,
1681 ValidationCode::UnknownField,
1682 ValidationCode::DuplicateField,
1683 ];
1684
1685 #[derive(Debug)]
1686 struct SecretSource;
1687
1688 impl fmt::Display for SecretSource {
1689 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1690 formatter.write_str("secret-canary-internal-context")
1691 }
1692 }
1693
1694 impl Error for SecretSource {}
1695
1696 #[test]
1697 fn application_error_registry_matches_the_checked_fixture() {
1698 let mut rendered = "code\tcategory\trecovery_action\tfixes\tmessage\n".to_owned();
1699 for code in APPLICATION_ERROR_CODES {
1700 let fixes = code
1701 .fixes()
1702 .iter()
1703 .map(|fix| fix.as_str())
1704 .collect::<Vec<_>>()
1705 .join(",");
1706 writeln!(
1707 rendered,
1708 "{}\t{}\t{}\t{}\t{}",
1709 code.as_str(),
1710 code.category().as_str(),
1711 code.recovery_action().as_str(),
1712 fixes,
1713 code.safe_message()
1714 )
1715 .expect("string");
1716 }
1717 assert_eq!(
1718 rendered,
1719 include_str!("../../../fixtures/application-errors/registry-v1.tsv")
1720 );
1721 }
1722
1723 #[test]
1724 fn application_hints_only_refine_compatible_kernel_failures() {
1725 let validation = PublicError::validation(ValidationIssues::one(ValidationIssue::new(
1726 ValidationCode::InvalidValue,
1727 ValidationPath::root(),
1728 )));
1729 let cursor = validation
1730 .clone()
1731 .with_application_code_hint(ApplicationErrorCode::CursorInvalid)
1732 .expect("compatible cursor refinement");
1733 assert_eq!(
1734 cursor.application_code_hint(),
1735 Some(ApplicationErrorCode::CursorInvalid)
1736 );
1737 assert_eq!(
1738 validation.with_application_code_hint(ApplicationErrorCode::StorageUnavailable),
1739 Err(ApplicationErrorHintError)
1740 );
1741 assert_eq!(
1742 PublicError::authorization_denied()
1743 .with_application_code_hint(ApplicationErrorCode::QueryInvalid),
1744 Err(ApplicationErrorHintError)
1745 );
1746 }
1747
1748 fn incident_id() -> IncidentId {
1749 let mut bytes = [0x42; 16];
1750 bytes[6] = 0x72;
1751 bytes[8] = 0x82;
1752 IncidentId::from_bytes(bytes).expect("fixture is a valid UUIDv7")
1753 }
1754
1755 fn issue() -> ValidationIssue {
1756 ValidationIssue::new(
1757 ValidationCode::TypeMismatch,
1758 ValidationPath::new(vec![
1759 ValidationPathSegment::Field(FieldId::new(7).expect("field ID is nonzero")),
1760 ValidationPathSegment::ListIndex(3),
1761 ])
1762 .expect("fixture path is bounded"),
1763 )
1764 }
1765
1766 #[test]
1767 fn codes_messages_classes_and_recovery_actions_are_stable() {
1768 let expected = [
1769 (
1770 "validation_failed",
1771 "request validation failed",
1772 ErrorClass::InvalidArgument,
1773 RecoveryAction::CorrectRequest,
1774 ),
1775 (
1776 "idempotency_key_reuse",
1777 "idempotency key was reused with different input",
1778 ErrorClass::Conflict,
1779 RecoveryAction::CorrectRequest,
1780 ),
1781 (
1782 "authorization_denied",
1783 "operation is not authorized",
1784 ErrorClass::PermissionDenied,
1785 RecoveryAction::ObtainPermission,
1786 ),
1787 (
1788 "concurrency_deadline_exceeded",
1789 "concurrency deadline exceeded",
1790 ErrorClass::DeadlineExceeded,
1791 RecoveryAction::Retry,
1792 ),
1793 (
1794 "contract_mismatch",
1795 "contract version or plan does not match",
1796 ErrorClass::FailedPrecondition,
1797 RecoveryAction::RefreshContract,
1798 ),
1799 (
1800 "storage_unavailable",
1801 "storage is temporarily unavailable",
1802 ErrorClass::Unavailable,
1803 RecoveryAction::Retry,
1804 ),
1805 (
1806 "outcome_unknown",
1807 "command outcome is not yet known",
1808 ErrorClass::Uncertain,
1809 RecoveryAction::ResolveWithSameIdempotencyKey,
1810 ),
1811 (
1812 "internal_defect",
1813 "an internal error occurred",
1814 ErrorClass::Internal,
1815 RecoveryAction::ContactOperator,
1816 ),
1817 (
1818 "command_execution_failed",
1819 "command execution failed",
1820 ErrorClass::FailedPrecondition,
1821 RecoveryAction::ContactOperator,
1822 ),
1823 (
1824 "history_incarnation_mismatch",
1825 "observed history predates a database restore",
1826 ErrorClass::FailedPrecondition,
1827 RecoveryAction::CorrectRequest,
1828 ),
1829 (
1830 "history_pruned",
1831 "requested history has been pruned",
1832 ErrorClass::FailedPrecondition,
1833 RecoveryAction::CorrectRequest,
1834 ),
1835 (
1836 "overloaded",
1837 "service is over capacity",
1838 ErrorClass::Unavailable,
1839 RecoveryAction::Retry,
1840 ),
1841 (
1842 "follower_mode",
1843 "operation is unavailable in follower mode",
1844 ErrorClass::FailedPrecondition,
1845 RecoveryAction::CorrectRequest,
1846 ),
1847 (
1848 "primary_fenced",
1849 "primary is durably fenced",
1850 ErrorClass::FailedPrecondition,
1851 RecoveryAction::ContactOperator,
1852 ),
1853 ];
1854
1855 assert_eq!(KINDS.len(), 14);
1856 assert_eq!(KINDS[9], PublicErrorKind::HistoryIncarnationMismatch);
1857 assert_eq!(KINDS[10], PublicErrorKind::HistoryPruned);
1858 assert_eq!(KINDS[11], PublicErrorKind::Overloaded);
1859 assert_eq!(APPLICATION_ERROR_CODES.len(), 27);
1860 assert_eq!(
1861 APPLICATION_ERROR_CODES[18],
1862 ApplicationErrorCode::HistoryIncarnationMismatch
1863 );
1864 assert_eq!(
1865 APPLICATION_ERROR_CODES[19],
1866 ApplicationErrorCode::HistoryPruned
1867 );
1868 assert_eq!(
1869 APPLICATION_ERROR_CODES[20],
1870 ApplicationErrorCode::Overloaded
1871 );
1872 assert_eq!(
1873 APPLICATION_ERROR_CODES[21],
1874 ApplicationErrorCode::ProjectionDiverged
1875 );
1876 assert_eq!(
1877 APPLICATION_ERROR_CODES[22],
1878 ApplicationErrorCode::SnapshotRetired
1879 );
1880 assert_eq!(
1881 APPLICATION_ERROR_CODES[23],
1882 ApplicationErrorCode::FreshnessUnsatisfied
1883 );
1884 assert_eq!(
1885 APPLICATION_ERROR_CODES[24],
1886 ApplicationErrorCode::ProjectedSourceRequired
1887 );
1888
1889 for (kind, (code, message, class, recovery_action)) in KINDS.into_iter().zip(expected) {
1890 assert_eq!(kind.code(), code);
1891 assert_eq!(kind.safe_message(), message);
1892 assert_eq!(kind.class(), class);
1893 assert_eq!(kind.recovery_action(), recovery_action);
1894 assert!(code.is_ascii());
1895 assert!(message.is_ascii());
1896 }
1897 }
1898
1899 #[test]
1900 fn every_kind_has_an_authoritative_wire_status_code() {
1901 let expected = [
1902 PublicErrorStatusCode::InvalidArgument,
1903 PublicErrorStatusCode::AlreadyExists,
1904 PublicErrorStatusCode::PermissionDenied,
1905 PublicErrorStatusCode::DeadlineExceeded,
1906 PublicErrorStatusCode::FailedPrecondition,
1907 PublicErrorStatusCode::Unavailable,
1908 PublicErrorStatusCode::Unknown,
1909 PublicErrorStatusCode::Internal,
1910 PublicErrorStatusCode::FailedPrecondition,
1911 PublicErrorStatusCode::FailedPrecondition,
1912 PublicErrorStatusCode::FailedPrecondition,
1913 PublicErrorStatusCode::ResourceExhausted,
1914 PublicErrorStatusCode::FailedPrecondition,
1915 ];
1916 for (kind, expected_code) in KINDS.into_iter().zip(expected) {
1917 assert_eq!(kind.status_code(), expected_code);
1918 assert_eq!(PublicError::contextless(kind).status_code(), expected_code);
1919 }
1920 assert_eq!(PublicErrorKind::Overloaded.class(), ErrorClass::Unavailable);
1921 assert_eq!(
1922 PublicErrorKind::Overloaded.status_code(),
1923 PublicErrorStatusCode::ResourceExhausted
1924 );
1925 }
1926
1927 #[test]
1928 fn staged_public_constructors_are_contextless_and_registry_aligned() {
1929 let mismatch = PublicError::history_incarnation_mismatch();
1930 assert_eq!(mismatch.kind(), PublicErrorKind::HistoryIncarnationMismatch);
1931 assert_eq!(mismatch.details(), &PublicErrorDetails::None);
1932 assert_eq!(mismatch.incident_id(), None);
1933 assert_eq!(
1934 ApplicationErrorCode::from_public_kind(mismatch.kind()),
1935 ApplicationErrorCode::HistoryIncarnationMismatch
1936 );
1937
1938 let pruned = PublicError::history_pruned();
1939 assert_eq!(pruned.kind(), PublicErrorKind::HistoryPruned);
1940 assert_eq!(pruned.details(), &PublicErrorDetails::None);
1941 assert_eq!(pruned.incident_id(), None);
1942 assert_eq!(
1943 ApplicationErrorCode::from_public_kind(pruned.kind()),
1944 ApplicationErrorCode::HistoryPruned
1945 );
1946
1947 let overloaded = PublicError::overloaded();
1948 assert_eq!(overloaded.kind(), PublicErrorKind::Overloaded);
1949 assert_eq!(overloaded.details(), &PublicErrorDetails::None);
1950 assert_eq!(overloaded.incident_id(), None);
1951 assert_eq!(
1952 ApplicationErrorCode::from_public_kind(overloaded.kind()),
1953 ApplicationErrorCode::Overloaded
1954 );
1955 }
1956
1957 #[test]
1958 fn concurrency_deadline_error_represents_both_v1_retryable_causes() {
1959 #[derive(Clone, Copy)]
1960 enum Cause {
1961 LogicalConflictDeadline,
1962 ReevaluationBudgetExhausted,
1963 }
1964
1965 const fn public_error_for(cause: Cause) -> PublicError {
1966 match cause {
1967 Cause::LogicalConflictDeadline | Cause::ReevaluationBudgetExhausted => {
1968 PublicError::concurrency_deadline_exceeded()
1969 }
1970 }
1971 }
1972
1973 for cause in [
1974 Cause::LogicalConflictDeadline,
1975 Cause::ReevaluationBudgetExhausted,
1976 ] {
1977 let error = public_error_for(cause);
1978 assert_eq!(error.kind(), PublicErrorKind::ConcurrencyDeadlineExceeded);
1979 assert_eq!(error.kind().code(), "concurrency_deadline_exceeded");
1980 assert_eq!(error.kind().safe_message(), "concurrency deadline exceeded");
1981 assert_eq!(error.kind().class(), ErrorClass::DeadlineExceeded);
1982 assert_eq!(error.kind().recovery_action(), RecoveryAction::Retry);
1983 assert_eq!(error.details(), &PublicErrorDetails::None);
1984 }
1985 }
1986
1987 #[test]
1988 fn validation_codes_and_messages_are_stable_and_safe() {
1989 let expected = [
1990 ("missing_required_value", "required value is missing"),
1991 ("type_mismatch", "value has the wrong type"),
1992 ("invalid_value", "value is invalid"),
1993 ("out_of_range", "value is outside the allowed range"),
1994 ("too_long", "value exceeds the allowed length"),
1995 ("too_many_items", "collection has too many items"),
1996 (
1997 "unknown_field",
1998 "field is not defined by the active contract",
1999 ),
2000 ("duplicate_field", "field occurs more than once"),
2001 ];
2002
2003 for (code, (stable_code, stable_message)) in VALIDATION_CODES.into_iter().zip(expected) {
2004 assert_eq!(code.code(), stable_code);
2005 assert_eq!(code.safe_message(), stable_message);
2006 assert!(stable_code.is_ascii());
2007 assert!(stable_message.is_ascii());
2008 }
2009 }
2010
2011 #[test]
2012 fn validation_paths_are_structured_and_bounded() {
2013 let boundary =
2014 vec![
2015 ValidationPathSegment::Field(FieldId::new(1).expect("field ID is nonzero"));
2016 MAX_VALIDATION_PATH_SEGMENTS
2017 ];
2018 let path = ValidationPath::new(boundary).expect("boundary is accepted");
2019 assert_eq!(path.segments().len(), MAX_VALIDATION_PATH_SEGMENTS);
2020 assert!(ValidationPath::root().segments().is_empty());
2021
2022 let above_boundary =
2023 vec![ValidationPathSegment::ListIndex(0); MAX_VALIDATION_PATH_SEGMENTS + 1];
2024 assert_eq!(
2025 ValidationPath::new(above_boundary),
2026 Err(ValidationPathError::TooManySegments)
2027 );
2028 }
2029
2030 #[test]
2031 fn validation_issue_collections_are_non_empty_and_bounded() {
2032 assert_eq!(
2033 ValidationIssues::new(Vec::new()),
2034 Err(ValidationIssuesError::Empty)
2035 );
2036
2037 let issues = ValidationIssues::new(vec![issue(); MAX_VALIDATION_ISSUES])
2038 .expect("boundary is accepted");
2039 assert_eq!(issues.as_slice().len(), MAX_VALIDATION_ISSUES);
2040
2041 assert_eq!(
2042 ValidationIssues::new(vec![issue(); MAX_VALIDATION_ISSUES + 1]),
2043 Err(ValidationIssuesError::TooManyIssues)
2044 );
2045 }
2046
2047 #[test]
2048 fn named_constructors_enforce_kind_and_detail_consistency() {
2049 let validation = PublicError::validation(ValidationIssues::one(issue()));
2050 assert_eq!(validation.kind(), PublicErrorKind::Validation);
2051 assert!(matches!(
2052 validation.details(),
2053 PublicErrorDetails::Validation(issues) if issues.as_slice().len() == 1
2054 ));
2055
2056 let active_version = ContractVersion::new(41).expect("contract version is nonzero");
2057 let mismatch = PublicError::contract_mismatch(active_version);
2058 assert_eq!(mismatch.kind(), PublicErrorKind::ContractMismatch);
2059 assert_eq!(
2060 mismatch.details(),
2061 &PublicErrorDetails::ContractMismatch {
2062 active_contract_version: active_version,
2063 }
2064 );
2065
2066 let contextless = [
2067 PublicError::idempotency_key_reuse(),
2068 PublicError::authorization_denied(),
2069 PublicError::concurrency_deadline_exceeded(),
2070 PublicError::storage_unavailable(),
2071 PublicError::outcome_unknown(),
2072 ];
2073 let expected = [
2074 PublicErrorKind::IdempotencyKeyReuse,
2075 PublicErrorKind::AuthorizationDenied,
2076 PublicErrorKind::ConcurrencyDeadlineExceeded,
2077 PublicErrorKind::StorageUnavailable,
2078 PublicErrorKind::OutcomeUnknown,
2079 ];
2080 for (error, kind) in contextless.into_iter().zip(expected) {
2081 assert_eq!(error.kind(), kind);
2082 assert_eq!(error.details(), &PublicErrorDetails::None);
2083 assert_eq!(error.incident_id(), None);
2084 }
2085
2086 let execution =
2087 PublicError::command_execution_failed(ExecutionFailureCode::ArithmeticFault);
2088 assert_eq!(execution.kind(), PublicErrorKind::CommandExecutionFailed);
2089 assert_eq!(
2090 execution.details(),
2091 &PublicErrorDetails::CommandExecutionFailed {
2092 code: ExecutionFailureCode::ArithmeticFault,
2093 }
2094 );
2095 assert_eq!(execution.incident_id(), None);
2096 }
2097
2098 #[test]
2099 fn public_error_never_exposes_a_source() {
2100 let error = PublicError::storage_unavailable();
2101
2102 assert!(error.source().is_none());
2103 }
2104
2105 #[test]
2106 fn internal_conversion_redacts_secret_source_everywhere_public() {
2107 let internal = InternalError::new(incident_id(), DefectScope::Process, SecretSource);
2108 assert!(internal.source().is_some());
2109 assert!(!internal.to_string().contains("secret-canary"));
2110 assert!(!format!("{internal:?}").contains("SecretSource"));
2111 assert_eq!(
2112 internal.source().map(ToString::to_string).as_deref(),
2113 Some("secret-canary-internal-context")
2114 );
2115
2116 let public = PublicError::from(internal);
2117 let display = public.to_string();
2118 let debug = format!("{public:?}");
2119
2120 assert_eq!(public.kind(), PublicErrorKind::InternalDefect);
2121 assert_eq!(public.details(), &PublicErrorDetails::None);
2122 assert_eq!(public.incident_id(), Some(&incident_id()));
2123 assert!(public.source().is_none());
2124 assert!(!display.contains("secret-canary"));
2125 assert!(!debug.contains("secret-canary"));
2126 assert!(!display.contains("SecretSource"));
2127 assert!(!debug.contains("SecretSource"));
2128 }
2129
2130 #[test]
2131 fn validation_details_cannot_carry_secret_canaries() {
2132 let error = PublicError::validation(ValidationIssues::one(issue()));
2133 let display = error.to_string();
2134 let debug = format!("{error:?}");
2135
2136 assert!(!display.contains("secret-canary"));
2137 assert!(!debug.contains("secret-canary"));
2138 assert_eq!(display, "validation_failed: request validation failed");
2139 }
2140
2141 #[test]
2142 fn public_display_contains_only_stable_fields() {
2143 let error = PublicError::internal_defect(incident_id());
2144
2145 assert_eq!(
2146 error.to_string(),
2147 "internal_defect: an internal error occurred (incident 42424242-4242-7242-8242-424242424242)"
2148 );
2149 }
2150
2151 #[test]
2158 fn public_error_details_shapes_carry_no_dynamic_payload_for_secret_fields() {
2159 let error = PublicError::validation(ValidationIssues::one(issue()));
2160 let PublicErrorDetails::Validation(issues) = error.details() else {
2162 panic!("validation details expected");
2163 };
2164 assert_eq!(
2165 issues.as_slice()[0].path().segments()[0],
2166 ValidationPathSegment::Field(FieldId::new(7).expect("nonzero field"))
2167 );
2168 for details in [
2170 PublicErrorDetails::None,
2171 error.details().clone(),
2172 PublicErrorDetails::ContractMismatch {
2173 active_contract_version: ContractVersion::new(3).expect("nonzero version"),
2174 },
2175 PublicErrorDetails::CommandExecutionFailed {
2176 code: ExecutionFailureCode::ArithmeticFault,
2177 },
2178 ] {
2179 match details {
2180 PublicErrorDetails::None => {}
2181 PublicErrorDetails::Validation(issues) => {
2182 for issue in issues.as_slice() {
2183 for segment in issue.path().segments() {
2184 match segment {
2185 ValidationPathSegment::Field(_)
2186 | ValidationPathSegment::ListIndex(_) => {}
2187 }
2188 }
2189 }
2190 }
2191 PublicErrorDetails::ContractMismatch {
2192 active_contract_version: _,
2193 } => {}
2194 PublicErrorDetails::CommandExecutionFailed { code: _ } => {}
2195 }
2196 }
2197 }
2198
2199 #[test]
2200 fn incident_source_is_synchronous_fallible_and_has_no_fallback() {
2201 struct FixedSource(IncidentId);
2202
2203 impl IncidentIdSource for FixedSource {
2204 fn next_incident_id(&self) -> Result<IncidentId, IncidentIdSourceError> {
2205 Ok(self.0)
2206 }
2207 }
2208
2209 struct FailingSource;
2210
2211 impl IncidentIdSource for FailingSource {
2212 fn next_incident_id(&self) -> Result<IncidentId, IncidentIdSourceError> {
2213 Err(IncidentIdSourceError)
2214 }
2215 }
2216
2217 assert_eq!(
2218 FixedSource(incident_id()).next_incident_id(),
2219 Ok(incident_id())
2220 );
2221 assert_eq!(FailingSource.next_incident_id(), Err(IncidentIdSourceError));
2222 assert_eq!(
2223 IncidentIdSourceError.to_string(),
2224 "incident identifier source failed"
2225 );
2226 }
2227
2228 #[test]
2229 fn emergency_internal_failure_has_no_identifier_or_disclosive_source() {
2230 let failure = EmergencyInternalFailure::from(IncidentIdSourceError);
2231
2232 assert_eq!(failure.safe_message(), "an internal error occurred");
2233 assert_eq!(failure.class(), ErrorClass::Internal);
2234 assert_eq!(failure.recovery_action(), RecoveryAction::ContactOperator);
2235 assert_eq!(failure.to_string(), "an internal error occurred");
2236 assert_eq!(format!("{failure:?}"), "EmergencyInternalFailure");
2237 assert!(failure.source().is_none());
2238 assert!(!failure.to_string().contains("incident"));
2239 }
2240}