#![forbid(unsafe_code)]
mod replication;
pub use replication::ReplicationStreamErrorV3;
use std::error::Error;
use std::fmt;
use riffdb_types::{
ContractLineage, ContractVersion, ExecutionFailureCode, FieldId, IncidentId, RequestId,
};
pub const APPLICATION_ERROR_ENVELOPE_VERSION: u32 = 1;
pub const MAX_APPLICATION_ERROR_BYTES: usize = 16 * 1024;
pub const MAX_APPLICATION_SYMBOL_PATH_SEGMENTS: usize = 16;
pub const MAX_APPLICATION_SYMBOL_BYTES: usize = 256;
pub const MAX_APPLICATION_FIXES: usize = 8;
pub const MAX_APPLICATION_SOURCE_OFFSET: u64 = 262_144;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ApplicationOperation {
DescribeContract,
CheckQuery,
ExplainQuery,
ExecuteQuery,
DeployQueryModule,
DeployReactiveModule,
GetQueryModule,
ExecuteCommand,
BatchCommand,
ExecuteProjectedQuery,
InspectVectorState,
}
impl ApplicationOperation {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::DescribeContract => "DescribeContract",
Self::CheckQuery => "CheckQuery",
Self::ExplainQuery => "ExplainQuery",
Self::ExecuteQuery => "ExecuteQuery",
Self::DeployQueryModule => "DeployQueryModule",
Self::DeployReactiveModule => "DeployReactiveModule",
Self::GetQueryModule => "GetQueryModule",
Self::ExecuteCommand => "ExecuteCommand",
Self::BatchCommand => "BatchCommand",
Self::ExecuteProjectedQuery => "ExecuteProjectedQuery",
Self::InspectVectorState => "InspectVectorState",
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ApplicationErrorCode {
InvalidRequest,
InputInvalid,
AuthorizationDenied,
ContractMismatch,
QueryInvalid,
QueryUnavailable,
ModuleUnavailable,
CursorInvalid,
ResponseTooLarge,
StorageUnavailable,
OutcomeUnknown,
OperationCancelled,
DeadlineExceeded,
InternalDefect,
IdempotencyKeyReuse,
CommandExecutionFailed,
CapabilityRevoked,
ProtocolInvalid,
HistoryIncarnationMismatch,
HistoryPruned,
Overloaded,
ProjectionDiverged,
SnapshotRetired,
FreshnessUnsatisfied,
ProjectedSourceRequired,
FollowerMode,
PrimaryFenced,
}
pub const APPLICATION_ERROR_CODES: [ApplicationErrorCode; 27] = [
ApplicationErrorCode::InvalidRequest,
ApplicationErrorCode::InputInvalid,
ApplicationErrorCode::AuthorizationDenied,
ApplicationErrorCode::ContractMismatch,
ApplicationErrorCode::QueryInvalid,
ApplicationErrorCode::QueryUnavailable,
ApplicationErrorCode::ModuleUnavailable,
ApplicationErrorCode::CursorInvalid,
ApplicationErrorCode::ResponseTooLarge,
ApplicationErrorCode::StorageUnavailable,
ApplicationErrorCode::OutcomeUnknown,
ApplicationErrorCode::OperationCancelled,
ApplicationErrorCode::DeadlineExceeded,
ApplicationErrorCode::InternalDefect,
ApplicationErrorCode::IdempotencyKeyReuse,
ApplicationErrorCode::CommandExecutionFailed,
ApplicationErrorCode::CapabilityRevoked,
ApplicationErrorCode::ProtocolInvalid,
ApplicationErrorCode::HistoryIncarnationMismatch,
ApplicationErrorCode::HistoryPruned,
ApplicationErrorCode::Overloaded,
ApplicationErrorCode::ProjectionDiverged,
ApplicationErrorCode::SnapshotRetired,
ApplicationErrorCode::FreshnessUnsatisfied,
ApplicationErrorCode::ProjectedSourceRequired,
ApplicationErrorCode::FollowerMode,
ApplicationErrorCode::PrimaryFenced,
];
impl ApplicationErrorCode {
#[must_use]
pub const fn from_public_kind(kind: PublicErrorKind) -> Self {
match kind {
PublicErrorKind::Validation => Self::InputInvalid,
PublicErrorKind::IdempotencyKeyReuse => Self::IdempotencyKeyReuse,
PublicErrorKind::AuthorizationDenied => Self::AuthorizationDenied,
PublicErrorKind::ConcurrencyDeadlineExceeded => Self::DeadlineExceeded,
PublicErrorKind::ContractMismatch => Self::ContractMismatch,
PublicErrorKind::StorageUnavailable => Self::StorageUnavailable,
PublicErrorKind::OutcomeUnknown => Self::OutcomeUnknown,
PublicErrorKind::InternalDefect => Self::InternalDefect,
PublicErrorKind::CommandExecutionFailed => Self::CommandExecutionFailed,
PublicErrorKind::HistoryIncarnationMismatch => Self::HistoryIncarnationMismatch,
PublicErrorKind::HistoryPruned => Self::HistoryPruned,
PublicErrorKind::Overloaded => Self::Overloaded,
PublicErrorKind::FollowerMode => Self::FollowerMode,
PublicErrorKind::PrimaryFenced => Self::PrimaryFenced,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::InvalidRequest => "RDB-APP-0001",
Self::InputInvalid => "RDB-INPUT-0101",
Self::AuthorizationDenied => "RDB-AUTH-0214",
Self::ContractMismatch => "RDB-CONTRACT-0101",
Self::QueryInvalid => "RDB-QUERY-0101",
Self::QueryUnavailable => "RDB-QUERY-0102",
Self::ModuleUnavailable => "RDB-MODULE-0101",
Self::CursorInvalid => "RDB-CURSOR-0101",
Self::ResponseTooLarge => "RDB-RESOURCE-0101",
Self::StorageUnavailable => "RDB-STORAGE-0101",
Self::OutcomeUnknown => "RDB-UNCERTAIN-0101",
Self::OperationCancelled => "RDB-APP-0002",
Self::DeadlineExceeded => "RDB-APP-0003",
Self::InternalDefect => "RDB-INTERNAL-0001",
Self::IdempotencyKeyReuse => "RDB-COMMAND-0101",
Self::CommandExecutionFailed => "RDB-COMMAND-0102",
Self::CapabilityRevoked => "RDB-AUTH-0215",
Self::ProtocolInvalid => "RDB-PROTOCOL-0101",
Self::HistoryIncarnationMismatch => "RDB-HISTORY-0101",
Self::HistoryPruned => "RDB-HISTORY-0102",
Self::Overloaded => "RDB-CAPACITY-0101",
Self::ProjectionDiverged => "RDB-PROJECTION-0101",
Self::SnapshotRetired => "RDB-PROJECTION-0102",
Self::FreshnessUnsatisfied => "RDB-PROJECTION-0103",
Self::ProjectedSourceRequired => "RDB-PROJECTION-0104",
Self::FollowerMode => "RDB-REP-0101",
Self::PrimaryFenced => "RDB-REP-0102",
}
}
#[must_use]
pub const fn safe_message(self) -> &'static str {
match self {
Self::InvalidRequest => "application request is structurally invalid",
Self::InputInvalid => "application input is invalid",
Self::AuthorizationDenied => "application operation is not authorized",
Self::ContractMismatch => "application contract does not match",
Self::QueryInvalid => "RiffQL query is invalid",
Self::QueryUnavailable => "RiffQL query is unavailable",
Self::ModuleUnavailable => "query module is unavailable",
Self::CursorInvalid => "query cursor is invalid or stale",
Self::ResponseTooLarge => "application result exceeds the service limit",
Self::StorageUnavailable => "storage is temporarily unavailable",
Self::OutcomeUnknown => "command outcome is not yet known",
Self::OperationCancelled => "application request was cancelled",
Self::DeadlineExceeded => "application request deadline elapsed",
Self::InternalDefect => "an internal error occurred",
Self::IdempotencyKeyReuse => {
"idempotency key was reused with different application input"
}
Self::CommandExecutionFailed => "command execution failed",
Self::CapabilityRevoked => "application capability is revoked",
Self::ProtocolInvalid => "the RiffDB peer returned an invalid application response",
Self::HistoryIncarnationMismatch => "observed history predates a database restore",
Self::HistoryPruned => "requested history has been pruned",
Self::Overloaded => "service is over capacity",
Self::FollowerMode => "operation is unavailable in follower mode",
Self::PrimaryFenced => "primary is durably fenced",
Self::ProjectionDiverged => "query projections cannot prove one common snapshot",
Self::SnapshotRetired => "the requested query snapshot has been retired",
Self::FreshnessUnsatisfied => "no query snapshot satisfies the requested freshness",
Self::ProjectedSourceRequired => {
"nearest query requires a compiler-owned projected source"
}
}
}
#[must_use]
pub const fn category(self) -> ApplicationErrorCategory {
match self {
Self::InvalidRequest | Self::InputInvalid => ApplicationErrorCategory::Input,
Self::AuthorizationDenied | Self::CapabilityRevoked => {
ApplicationErrorCategory::Authorization
}
Self::ContractMismatch => ApplicationErrorCategory::Contract,
Self::QueryInvalid
| Self::QueryUnavailable
| Self::ProjectionDiverged
| Self::SnapshotRetired
| Self::FreshnessUnsatisfied => ApplicationErrorCategory::Query,
Self::ProjectedSourceRequired => ApplicationErrorCategory::Query,
Self::ModuleUnavailable => ApplicationErrorCategory::Module,
Self::CursorInvalid => ApplicationErrorCategory::Cursor,
Self::ResponseTooLarge => ApplicationErrorCategory::Resource,
Self::StorageUnavailable => ApplicationErrorCategory::Storage,
Self::OutcomeUnknown => ApplicationErrorCategory::Uncertainty,
Self::OperationCancelled
| Self::DeadlineExceeded
| Self::FollowerMode
| Self::PrimaryFenced => ApplicationErrorCategory::Control,
Self::InternalDefect => ApplicationErrorCategory::Internal,
Self::IdempotencyKeyReuse | Self::CommandExecutionFailed => {
ApplicationErrorCategory::Command
}
Self::ProtocolInvalid => ApplicationErrorCategory::Protocol,
Self::HistoryIncarnationMismatch | Self::HistoryPruned => {
ApplicationErrorCategory::History
}
Self::Overloaded => ApplicationErrorCategory::Capacity,
}
}
#[must_use]
pub const fn recovery_action(self) -> ApplicationRecoveryAction {
match self {
Self::InvalidRequest
| Self::InputInvalid
| Self::QueryInvalid
| Self::CursorInvalid
| Self::ResponseTooLarge
| Self::IdempotencyKeyReuse
| Self::HistoryIncarnationMismatch
| Self::HistoryPruned
| Self::SnapshotRetired
| Self::FollowerMode => ApplicationRecoveryAction::CorrectRequest,
Self::ProjectedSourceRequired => ApplicationRecoveryAction::RefreshContract,
Self::AuthorizationDenied | Self::CapabilityRevoked => {
ApplicationRecoveryAction::ObtainPermission
}
Self::ContractMismatch | Self::QueryUnavailable | Self::ModuleUnavailable => {
ApplicationRecoveryAction::RefreshContract
}
Self::StorageUnavailable
| Self::DeadlineExceeded
| Self::Overloaded
| Self::ProjectionDiverged
| Self::FreshnessUnsatisfied => ApplicationRecoveryAction::Retry,
Self::OutcomeUnknown => ApplicationRecoveryAction::ResolveWithSameIdempotencyKey,
Self::OperationCancelled => ApplicationRecoveryAction::None,
Self::InternalDefect
| Self::CommandExecutionFailed
| Self::ProtocolInvalid
| Self::PrimaryFenced => ApplicationRecoveryAction::ContactOperator,
}
}
#[must_use]
pub const fn fixes(self) -> &'static [ApplicationFixCode] {
match self {
Self::InvalidRequest
| Self::InputInvalid
| Self::QueryInvalid
| Self::HistoryIncarnationMismatch
| Self::HistoryPruned => &[ApplicationFixCode::CorrectInput],
Self::AuthorizationDenied | Self::CapabilityRevoked => {
&[ApplicationFixCode::BindApplicationRole]
}
Self::ContractMismatch => &[ApplicationFixCode::RefreshContract],
Self::QueryUnavailable | Self::ModuleUnavailable => {
&[ApplicationFixCode::PinActiveModule]
}
Self::ProjectedSourceRequired => &[ApplicationFixCode::PinActiveModule],
Self::CursorInvalid => &[ApplicationFixCode::RestartFromFirstPage],
Self::SnapshotRetired => &[ApplicationFixCode::RestartFromFirstPage],
Self::ResponseTooLarge | Self::IdempotencyKeyReuse => {
&[ApplicationFixCode::CorrectInput]
}
Self::StorageUnavailable
| Self::DeadlineExceeded
| Self::Overloaded
| Self::ProjectionDiverged
| Self::FreshnessUnsatisfied => &[ApplicationFixCode::RetryLater],
Self::OutcomeUnknown => &[ApplicationFixCode::ResolveWithSameIdempotencyKey],
Self::InternalDefect => &[ApplicationFixCode::ContactOperatorWithIncident],
Self::OperationCancelled
| Self::CommandExecutionFailed
| Self::ProtocolInvalid
| Self::FollowerMode
| Self::PrimaryFenced => &[],
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ApplicationErrorCategory {
Input,
Authorization,
Contract,
Query,
Module,
Cursor,
Resource,
Storage,
Uncertainty,
Internal,
Protocol,
Command,
Control,
History,
Capacity,
}
impl ApplicationErrorCategory {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Input => "input",
Self::Authorization => "authorization",
Self::Contract => "contract",
Self::Query => "query",
Self::Module => "module",
Self::Cursor => "cursor",
Self::Resource => "resource",
Self::Storage => "storage",
Self::Uncertainty => "uncertainty",
Self::Internal => "internal",
Self::Protocol => "protocol",
Self::Command => "command",
Self::Control => "control",
Self::History => "history",
Self::Capacity => "capacity",
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ApplicationRecoveryAction {
CorrectRequest,
Retry,
ResolveWithSameIdempotencyKey,
ObtainPermission,
RefreshContract,
ContactOperator,
None,
}
impl ApplicationRecoveryAction {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::CorrectRequest => "correct_request",
Self::Retry => "retry",
Self::ResolveWithSameIdempotencyKey => "resolve_with_same_idempotency_key",
Self::ObtainPermission => "obtain_permission",
Self::RefreshContract => "refresh_contract",
Self::ContactOperator => "contact_operator",
Self::None => "none",
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ApplicationFixCode {
CorrectInput,
RemoveForbiddenOutput,
BindApplicationRole,
RefreshContract,
PinActiveModule,
AddBoundedIndex,
RestartFromFirstPage,
RetryLater,
ResolveWithSameIdempotencyKey,
ContactOperatorWithIncident,
}
impl ApplicationFixCode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::CorrectInput => "correct_input",
Self::RemoveForbiddenOutput => "remove_forbidden_output",
Self::BindApplicationRole => "bind_application_role",
Self::RefreshContract => "refresh_contract",
Self::PinActiveModule => "pin_active_module",
Self::AddBoundedIndex => "add_bounded_index",
Self::RestartFromFirstPage => "restart_from_first_page",
Self::RetryLater => "retry_later",
Self::ResolveWithSameIdempotencyKey => "resolve_with_same_idempotency_key",
Self::ContactOperatorWithIncident => "contact_operator_with_incident",
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct ApplicationSourceSpan {
start: u64,
end: u64,
}
impl ApplicationSourceSpan {
pub const fn new(start: u64, end: u64) -> Option<Self> {
if start <= end && end <= MAX_APPLICATION_SOURCE_OFFSET {
Some(Self { start, end })
} else {
None
}
}
#[must_use]
pub const fn start(self) -> u64 {
self.start
}
#[must_use]
pub const fn end(self) -> u64 {
self.end
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ApplicationErrorContext {
contract: Option<(ContractLineage, ContractVersion)>,
operation_symbol: Option<String>,
symbol_path: Vec<String>,
source_span: Option<ApplicationSourceSpan>,
trace_id: Option<RequestId>,
}
impl ApplicationErrorContext {
#[must_use]
pub const fn empty() -> Self {
Self {
contract: None,
operation_symbol: None,
symbol_path: Vec::new(),
source_span: None,
trace_id: None,
}
}
#[must_use]
pub fn with_contract(mut self, lineage: ContractLineage, version: ContractVersion) -> Self {
self.contract = Some((lineage, version));
self
}
pub fn with_operation_symbol(
mut self,
symbol: String,
) -> Result<Self, ApplicationErrorContextError> {
validate_application_symbol(&symbol)?;
self.operation_symbol = Some(symbol);
Ok(self)
}
pub fn with_symbol_path(
mut self,
symbols: Vec<String>,
) -> Result<Self, ApplicationErrorContextError> {
if symbols.len() > MAX_APPLICATION_SYMBOL_PATH_SEGMENTS {
return Err(ApplicationErrorContextError);
}
for symbol in &symbols {
validate_application_symbol(symbol)?;
}
self.symbol_path = symbols;
Ok(self)
}
#[must_use]
pub const fn with_source_span(mut self, source_span: ApplicationSourceSpan) -> Self {
self.source_span = Some(source_span);
self
}
#[must_use]
pub const fn with_trace_id(mut self, trace_id: RequestId) -> Self {
self.trace_id = Some(trace_id);
self
}
#[must_use]
pub const fn contract(&self) -> Option<&(ContractLineage, ContractVersion)> {
self.contract.as_ref()
}
#[must_use]
pub fn operation_symbol(&self) -> Option<&str> {
self.operation_symbol.as_deref()
}
#[must_use]
pub fn symbol_path(&self) -> &[String] {
&self.symbol_path
}
#[must_use]
pub const fn source_span(&self) -> Option<ApplicationSourceSpan> {
self.source_span
}
#[must_use]
pub const fn trace_id(&self) -> Option<RequestId> {
self.trace_id
}
}
fn validate_application_symbol(symbol: &str) -> Result<(), ApplicationErrorContextError> {
if symbol.is_empty()
|| symbol.len() > MAX_APPLICATION_SYMBOL_BYTES
|| !symbol
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
{
return Err(ApplicationErrorContextError);
}
Ok(())
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ApplicationErrorContextError;
impl fmt::Display for ApplicationErrorContextError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("application error context is invalid")
}
}
impl Error for ApplicationErrorContextError {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ApplicationError {
code: ApplicationErrorCode,
operation: ApplicationOperation,
context: ApplicationErrorContext,
incident_id: Option<IncidentId>,
}
impl ApplicationError {
#[must_use]
pub const fn new(
code: ApplicationErrorCode,
operation: ApplicationOperation,
context: ApplicationErrorContext,
incident_id: Option<IncidentId>,
) -> Self {
Self {
code,
operation,
context,
incident_id,
}
}
#[must_use]
pub fn from_public_error(
error: &PublicError,
operation: ApplicationOperation,
context: ApplicationErrorContext,
) -> Self {
Self::new(
error
.application_code_hint()
.unwrap_or_else(|| ApplicationErrorCode::from_public_kind(error.kind())),
operation,
context,
error.incident_id().copied(),
)
}
#[must_use]
pub const fn code(&self) -> ApplicationErrorCode {
self.code
}
#[must_use]
pub const fn operation(&self) -> ApplicationOperation {
self.operation
}
#[must_use]
pub const fn context(&self) -> &ApplicationErrorContext {
&self.context
}
#[must_use]
pub const fn incident_id(&self) -> Option<&IncidentId> {
self.incident_id.as_ref()
}
#[must_use]
pub const fn category(&self) -> ApplicationErrorCategory {
self.code.category()
}
#[must_use]
pub const fn safe_message(&self) -> &'static str {
self.code.safe_message()
}
#[must_use]
pub const fn recovery_action(&self) -> ApplicationRecoveryAction {
self.code.recovery_action()
}
#[must_use]
pub const fn fixes(&self) -> &'static [ApplicationFixCode] {
self.code.fixes()
}
}
impl fmt::Display for ApplicationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{}: {} [{}]",
self.code.as_str(),
self.safe_message(),
self.operation.as_str()
)?;
if let Some(symbol) = self.context.operation_symbol() {
write!(formatter, " {symbol}")?;
}
if let Some(incident_id) = &self.incident_id {
write!(formatter, " (incident {incident_id})")?;
}
Ok(())
}
}
impl Error for ApplicationError {}
pub trait IncidentIdSource: Send + Sync {
fn next_incident_id(&self) -> Result<IncidentId, IncidentIdSourceError>;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct IncidentIdSourceError;
impl fmt::Display for IncidentIdSourceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("incident identifier source failed")
}
}
impl Error for IncidentIdSourceError {}
pub const MAX_VALIDATION_ISSUES: usize = 16;
pub const MAX_VALIDATION_PATH_SEGMENTS: usize = 16;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ErrorClass {
InvalidArgument,
Conflict,
PermissionDenied,
DeadlineExceeded,
FailedPrecondition,
Unavailable,
Uncertain,
Internal,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RecoveryAction {
CorrectRequest,
Retry,
ResolveWithSameIdempotencyKey,
ObtainPermission,
RefreshContract,
ContactOperator,
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct EmergencyInternalFailure {
_private: (),
}
impl EmergencyInternalFailure {
#[must_use]
pub const fn safe_message(self) -> &'static str {
"an internal error occurred"
}
#[must_use]
pub const fn class(self) -> ErrorClass {
ErrorClass::Internal
}
#[must_use]
pub const fn recovery_action(self) -> RecoveryAction {
RecoveryAction::ContactOperator
}
}
impl From<IncidentIdSourceError> for EmergencyInternalFailure {
fn from(_: IncidentIdSourceError) -> Self {
Self { _private: () }
}
}
impl fmt::Display for EmergencyInternalFailure {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.safe_message())
}
}
impl fmt::Debug for EmergencyInternalFailure {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("EmergencyInternalFailure")
}
}
impl Error for EmergencyInternalFailure {}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum PublicErrorKind {
Validation,
IdempotencyKeyReuse,
AuthorizationDenied,
ConcurrencyDeadlineExceeded,
ContractMismatch,
StorageUnavailable,
OutcomeUnknown,
InternalDefect,
CommandExecutionFailed,
HistoryIncarnationMismatch,
HistoryPruned,
Overloaded,
FollowerMode,
PrimaryFenced,
}
impl PublicErrorKind {
#[must_use]
pub const fn code(self) -> &'static str {
match self {
Self::Validation => "validation_failed",
Self::IdempotencyKeyReuse => "idempotency_key_reuse",
Self::AuthorizationDenied => "authorization_denied",
Self::ConcurrencyDeadlineExceeded => "concurrency_deadline_exceeded",
Self::ContractMismatch => "contract_mismatch",
Self::StorageUnavailable => "storage_unavailable",
Self::OutcomeUnknown => "outcome_unknown",
Self::InternalDefect => "internal_defect",
Self::CommandExecutionFailed => "command_execution_failed",
Self::HistoryIncarnationMismatch => "history_incarnation_mismatch",
Self::HistoryPruned => "history_pruned",
Self::Overloaded => "overloaded",
Self::FollowerMode => "follower_mode",
Self::PrimaryFenced => "primary_fenced",
}
}
#[must_use]
pub const fn safe_message(self) -> &'static str {
match self {
Self::Validation => "request validation failed",
Self::IdempotencyKeyReuse => "idempotency key was reused with different input",
Self::AuthorizationDenied => "operation is not authorized",
Self::ConcurrencyDeadlineExceeded => "concurrency deadline exceeded",
Self::ContractMismatch => "contract version or plan does not match",
Self::StorageUnavailable => "storage is temporarily unavailable",
Self::OutcomeUnknown => "command outcome is not yet known",
Self::InternalDefect => "an internal error occurred",
Self::CommandExecutionFailed => "command execution failed",
Self::HistoryIncarnationMismatch => "observed history predates a database restore",
Self::HistoryPruned => "requested history has been pruned",
Self::Overloaded => "service is over capacity",
Self::FollowerMode => "operation is unavailable in follower mode",
Self::PrimaryFenced => "primary is durably fenced",
}
}
#[must_use]
pub const fn class(self) -> ErrorClass {
match self {
Self::Validation => ErrorClass::InvalidArgument,
Self::IdempotencyKeyReuse => ErrorClass::Conflict,
Self::AuthorizationDenied => ErrorClass::PermissionDenied,
Self::ConcurrencyDeadlineExceeded => ErrorClass::DeadlineExceeded,
Self::ContractMismatch
| Self::HistoryIncarnationMismatch
| Self::HistoryPruned
| Self::FollowerMode
| Self::PrimaryFenced => ErrorClass::FailedPrecondition,
Self::StorageUnavailable | Self::Overloaded => ErrorClass::Unavailable,
Self::OutcomeUnknown => ErrorClass::Uncertain,
Self::InternalDefect => ErrorClass::Internal,
Self::CommandExecutionFailed => ErrorClass::FailedPrecondition,
}
}
#[must_use]
pub const fn status_code(self) -> PublicErrorStatusCode {
match self {
Self::Validation => PublicErrorStatusCode::InvalidArgument,
Self::IdempotencyKeyReuse => PublicErrorStatusCode::AlreadyExists,
Self::AuthorizationDenied => PublicErrorStatusCode::PermissionDenied,
Self::ConcurrencyDeadlineExceeded => PublicErrorStatusCode::DeadlineExceeded,
Self::ContractMismatch
| Self::CommandExecutionFailed
| Self::HistoryIncarnationMismatch
| Self::HistoryPruned
| Self::FollowerMode
| Self::PrimaryFenced => PublicErrorStatusCode::FailedPrecondition,
Self::StorageUnavailable => PublicErrorStatusCode::Unavailable,
Self::Overloaded => PublicErrorStatusCode::ResourceExhausted,
Self::OutcomeUnknown => PublicErrorStatusCode::Unknown,
Self::InternalDefect => PublicErrorStatusCode::Internal,
}
}
#[must_use]
pub const fn recovery_action(self) -> RecoveryAction {
match self {
Self::Validation
| Self::IdempotencyKeyReuse
| Self::HistoryIncarnationMismatch
| Self::HistoryPruned
| Self::FollowerMode => RecoveryAction::CorrectRequest,
Self::AuthorizationDenied => RecoveryAction::ObtainPermission,
Self::ConcurrencyDeadlineExceeded | Self::StorageUnavailable | Self::Overloaded => {
RecoveryAction::Retry
}
Self::ContractMismatch => RecoveryAction::RefreshContract,
Self::OutcomeUnknown => RecoveryAction::ResolveWithSameIdempotencyKey,
Self::InternalDefect | Self::CommandExecutionFailed | Self::PrimaryFenced => {
RecoveryAction::ContactOperator
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum PublicErrorStatusCode {
InvalidArgument,
AlreadyExists,
PermissionDenied,
DeadlineExceeded,
FailedPrecondition,
ResourceExhausted,
Unavailable,
Unknown,
Internal,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ValidationCode {
MissingRequiredValue,
TypeMismatch,
InvalidValue,
OutOfRange,
TooLong,
TooManyItems,
UnknownField,
DuplicateField,
}
impl ValidationCode {
#[must_use]
pub const fn code(self) -> &'static str {
match self {
Self::MissingRequiredValue => "missing_required_value",
Self::TypeMismatch => "type_mismatch",
Self::InvalidValue => "invalid_value",
Self::OutOfRange => "out_of_range",
Self::TooLong => "too_long",
Self::TooManyItems => "too_many_items",
Self::UnknownField => "unknown_field",
Self::DuplicateField => "duplicate_field",
}
}
#[must_use]
pub const fn safe_message(self) -> &'static str {
match self {
Self::MissingRequiredValue => "required value is missing",
Self::TypeMismatch => "value has the wrong type",
Self::InvalidValue => "value is invalid",
Self::OutOfRange => "value is outside the allowed range",
Self::TooLong => "value exceeds the allowed length",
Self::TooManyItems => "collection has too many items",
Self::UnknownField => "field is not defined by the active contract",
Self::DuplicateField => "field occurs more than once",
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ValidationPathSegment {
Field(FieldId),
ListIndex(u32),
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct ValidationPath {
segments: Vec<ValidationPathSegment>,
}
impl ValidationPath {
#[must_use]
pub const fn root() -> Self {
Self {
segments: Vec::new(),
}
}
pub fn new(segments: Vec<ValidationPathSegment>) -> Result<Self, ValidationPathError> {
if segments.len() > MAX_VALIDATION_PATH_SEGMENTS {
return Err(ValidationPathError::TooManySegments);
}
Ok(Self { segments })
}
#[must_use]
pub fn segments(&self) -> &[ValidationPathSegment] {
&self.segments
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ValidationPathError {
TooManySegments,
}
impl fmt::Display for ValidationPathError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("validation path has too many segments")
}
}
impl Error for ValidationPathError {}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ValidationIssue {
code: ValidationCode,
path: ValidationPath,
}
impl ValidationIssue {
#[must_use]
pub const fn new(code: ValidationCode, path: ValidationPath) -> Self {
Self { code, path }
}
#[must_use]
pub const fn code(&self) -> ValidationCode {
self.code
}
#[must_use]
pub const fn path(&self) -> &ValidationPath {
&self.path
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ValidationIssues {
issues: Vec<ValidationIssue>,
}
impl ValidationIssues {
pub fn new(issues: Vec<ValidationIssue>) -> Result<Self, ValidationIssuesError> {
if issues.is_empty() {
return Err(ValidationIssuesError::Empty);
}
if issues.len() > MAX_VALIDATION_ISSUES {
return Err(ValidationIssuesError::TooManyIssues);
}
Ok(Self { issues })
}
#[must_use]
pub fn one(issue: ValidationIssue) -> Self {
Self {
issues: vec![issue],
}
}
#[must_use]
pub fn as_slice(&self) -> &[ValidationIssue] {
&self.issues
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ValidationIssuesError {
Empty,
TooManyIssues,
}
impl fmt::Display for ValidationIssuesError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Empty => "validation issue collection is empty",
Self::TooManyIssues => "validation issue collection has too many entries",
})
}
}
impl Error for ValidationIssuesError {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PublicErrorDetails {
None,
Validation(ValidationIssues),
ContractMismatch {
active_contract_version: ContractVersion,
},
CommandExecutionFailed {
code: ExecutionFailureCode,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PublicError {
kind: PublicErrorKind,
details: PublicErrorDetails,
incident_id: Option<IncidentId>,
application_code_hint: Option<ApplicationErrorCode>,
}
impl PublicError {
const fn contextless(kind: PublicErrorKind) -> Self {
Self {
kind,
details: PublicErrorDetails::None,
incident_id: None,
application_code_hint: None,
}
}
#[must_use]
pub const fn validation(issues: ValidationIssues) -> Self {
Self {
kind: PublicErrorKind::Validation,
details: PublicErrorDetails::Validation(issues),
incident_id: None,
application_code_hint: None,
}
}
#[must_use]
pub const fn idempotency_key_reuse() -> Self {
Self::contextless(PublicErrorKind::IdempotencyKeyReuse)
}
#[must_use]
pub const fn authorization_denied() -> Self {
Self::contextless(PublicErrorKind::AuthorizationDenied)
}
#[must_use]
pub const fn concurrency_deadline_exceeded() -> Self {
Self::contextless(PublicErrorKind::ConcurrencyDeadlineExceeded)
}
#[must_use]
pub const fn contract_mismatch(active_contract_version: ContractVersion) -> Self {
Self {
kind: PublicErrorKind::ContractMismatch,
details: PublicErrorDetails::ContractMismatch {
active_contract_version,
},
incident_id: None,
application_code_hint: None,
}
}
#[must_use]
pub const fn storage_unavailable() -> Self {
Self::contextless(PublicErrorKind::StorageUnavailable)
}
#[must_use]
pub const fn outcome_unknown() -> Self {
Self::contextless(PublicErrorKind::OutcomeUnknown)
}
#[must_use]
pub const fn command_execution_failed(code: ExecutionFailureCode) -> Self {
Self {
kind: PublicErrorKind::CommandExecutionFailed,
details: PublicErrorDetails::CommandExecutionFailed { code },
incident_id: None,
application_code_hint: None,
}
}
#[must_use]
pub const fn internal_defect(incident_id: IncidentId) -> Self {
Self {
kind: PublicErrorKind::InternalDefect,
details: PublicErrorDetails::None,
incident_id: Some(incident_id),
application_code_hint: None,
}
}
#[must_use]
pub const fn history_incarnation_mismatch() -> Self {
Self::contextless(PublicErrorKind::HistoryIncarnationMismatch)
}
#[must_use]
pub const fn history_pruned() -> Self {
Self::contextless(PublicErrorKind::HistoryPruned)
}
#[must_use]
pub const fn follower_mode() -> Self {
Self::contextless(PublicErrorKind::FollowerMode)
}
#[must_use]
pub const fn primary_fenced() -> Self {
Self::contextless(PublicErrorKind::PrimaryFenced)
}
#[must_use]
pub const fn overloaded() -> Self {
Self::contextless(PublicErrorKind::Overloaded)
}
#[must_use]
pub const fn with_incident_id(mut self, incident_id: IncidentId) -> Self {
self.incident_id = Some(incident_id);
self
}
pub fn with_application_code_hint(
mut self,
code: ApplicationErrorCode,
) -> Result<Self, ApplicationErrorHintError> {
let compatible = match self.kind {
PublicErrorKind::Validation => matches!(
code,
ApplicationErrorCode::InvalidRequest
| ApplicationErrorCode::InputInvalid
| ApplicationErrorCode::QueryInvalid
| ApplicationErrorCode::QueryUnavailable
| ApplicationErrorCode::ModuleUnavailable
| ApplicationErrorCode::CursorInvalid
| ApplicationErrorCode::ProjectionDiverged
| ApplicationErrorCode::SnapshotRetired
| ApplicationErrorCode::FreshnessUnsatisfied
| ApplicationErrorCode::ProjectedSourceRequired
),
PublicErrorKind::AuthorizationDenied => matches!(
code,
ApplicationErrorCode::AuthorizationDenied | ApplicationErrorCode::CapabilityRevoked
),
PublicErrorKind::ContractMismatch => code == ApplicationErrorCode::ContractMismatch,
PublicErrorKind::StorageUnavailable => code == ApplicationErrorCode::StorageUnavailable,
PublicErrorKind::OutcomeUnknown => code == ApplicationErrorCode::OutcomeUnknown,
PublicErrorKind::InternalDefect => code == ApplicationErrorCode::InternalDefect,
PublicErrorKind::IdempotencyKeyReuse => {
code == ApplicationErrorCode::IdempotencyKeyReuse
}
PublicErrorKind::ConcurrencyDeadlineExceeded => {
code == ApplicationErrorCode::DeadlineExceeded
}
PublicErrorKind::CommandExecutionFailed => {
code == ApplicationErrorCode::CommandExecutionFailed
}
PublicErrorKind::HistoryIncarnationMismatch => {
code == ApplicationErrorCode::HistoryIncarnationMismatch
}
PublicErrorKind::HistoryPruned => code == ApplicationErrorCode::HistoryPruned,
PublicErrorKind::Overloaded => code == ApplicationErrorCode::Overloaded,
PublicErrorKind::FollowerMode => code == ApplicationErrorCode::FollowerMode,
PublicErrorKind::PrimaryFenced => code == ApplicationErrorCode::PrimaryFenced,
};
if !compatible {
return Err(ApplicationErrorHintError);
}
self.application_code_hint = Some(code);
Ok(self)
}
#[must_use]
pub const fn application_code_hint(&self) -> Option<ApplicationErrorCode> {
self.application_code_hint
}
#[must_use]
pub const fn kind(&self) -> PublicErrorKind {
self.kind
}
#[must_use]
pub const fn code(&self) -> &'static str {
self.kind.code()
}
#[must_use]
pub const fn safe_message(&self) -> &'static str {
self.kind.safe_message()
}
#[must_use]
pub const fn class(&self) -> ErrorClass {
self.kind.class()
}
#[must_use]
pub const fn status_code(&self) -> PublicErrorStatusCode {
self.kind.status_code()
}
#[must_use]
pub const fn recovery_action(&self) -> RecoveryAction {
self.kind.recovery_action()
}
#[must_use]
pub const fn details(&self) -> &PublicErrorDetails {
&self.details
}
#[must_use]
pub const fn incident_id(&self) -> Option<&IncidentId> {
self.incident_id.as_ref()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ApplicationErrorHintError;
impl fmt::Display for ApplicationErrorHintError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("application error hint is incompatible")
}
}
impl Error for ApplicationErrorHintError {}
impl fmt::Display for PublicError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}: {}", self.code(), self.safe_message())?;
if let Some(incident_id) = &self.incident_id {
write!(formatter, " (incident {incident_id})")?;
}
Ok(())
}
}
impl Error for PublicError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DefectScope {
Process,
Request,
}
pub struct InternalError {
incident_id: IncidentId,
scope: DefectScope,
source: Box<dyn Error + Send + Sync + 'static>,
}
impl InternalError {
pub fn new(
incident_id: IncidentId,
scope: DefectScope,
source: impl Error + Send + Sync + 'static,
) -> Self {
Self {
incident_id,
scope,
source: Box::new(source),
}
}
#[must_use]
pub const fn scope(&self) -> DefectScope {
self.scope
}
#[must_use]
pub const fn incident_id(&self) -> &IncidentId {
&self.incident_id
}
#[must_use]
pub fn into_public(self) -> PublicError {
PublicError::internal_defect(self.incident_id)
}
}
impl fmt::Display for InternalError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "internal defect (incident {})", self.incident_id)
}
}
impl fmt::Debug for InternalError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("InternalError")
.field("incident_id", &self.incident_id)
.field("source", &"[REDACTED]")
.finish()
}
}
impl Error for InternalError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(self.source.as_ref())
}
}
impl From<InternalError> for PublicError {
fn from(error: InternalError) -> Self {
error.into_public()
}
}
#[cfg(test)]
mod tests {
use std::fmt::Write as _;
use super::*;
const KINDS: [PublicErrorKind; 14] = [
PublicErrorKind::Validation,
PublicErrorKind::IdempotencyKeyReuse,
PublicErrorKind::AuthorizationDenied,
PublicErrorKind::ConcurrencyDeadlineExceeded,
PublicErrorKind::ContractMismatch,
PublicErrorKind::StorageUnavailable,
PublicErrorKind::OutcomeUnknown,
PublicErrorKind::InternalDefect,
PublicErrorKind::CommandExecutionFailed,
PublicErrorKind::HistoryIncarnationMismatch,
PublicErrorKind::HistoryPruned,
PublicErrorKind::Overloaded,
PublicErrorKind::FollowerMode,
PublicErrorKind::PrimaryFenced,
];
const VALIDATION_CODES: [ValidationCode; 8] = [
ValidationCode::MissingRequiredValue,
ValidationCode::TypeMismatch,
ValidationCode::InvalidValue,
ValidationCode::OutOfRange,
ValidationCode::TooLong,
ValidationCode::TooManyItems,
ValidationCode::UnknownField,
ValidationCode::DuplicateField,
];
#[derive(Debug)]
struct SecretSource;
impl fmt::Display for SecretSource {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("secret-canary-internal-context")
}
}
impl Error for SecretSource {}
#[test]
fn application_error_registry_matches_the_checked_fixture() {
let mut rendered = "code\tcategory\trecovery_action\tfixes\tmessage\n".to_owned();
for code in APPLICATION_ERROR_CODES {
let fixes = code
.fixes()
.iter()
.map(|fix| fix.as_str())
.collect::<Vec<_>>()
.join(",");
writeln!(
rendered,
"{}\t{}\t{}\t{}\t{}",
code.as_str(),
code.category().as_str(),
code.recovery_action().as_str(),
fixes,
code.safe_message()
)
.expect("string");
}
assert_eq!(
rendered,
include_str!("../../../fixtures/application-errors/registry-v1.tsv")
);
}
#[test]
fn application_hints_only_refine_compatible_kernel_failures() {
let validation = PublicError::validation(ValidationIssues::one(ValidationIssue::new(
ValidationCode::InvalidValue,
ValidationPath::root(),
)));
let cursor = validation
.clone()
.with_application_code_hint(ApplicationErrorCode::CursorInvalid)
.expect("compatible cursor refinement");
assert_eq!(
cursor.application_code_hint(),
Some(ApplicationErrorCode::CursorInvalid)
);
assert_eq!(
validation.with_application_code_hint(ApplicationErrorCode::StorageUnavailable),
Err(ApplicationErrorHintError)
);
assert_eq!(
PublicError::authorization_denied()
.with_application_code_hint(ApplicationErrorCode::QueryInvalid),
Err(ApplicationErrorHintError)
);
}
fn incident_id() -> IncidentId {
let mut bytes = [0x42; 16];
bytes[6] = 0x72;
bytes[8] = 0x82;
IncidentId::from_bytes(bytes).expect("fixture is a valid UUIDv7")
}
fn issue() -> ValidationIssue {
ValidationIssue::new(
ValidationCode::TypeMismatch,
ValidationPath::new(vec![
ValidationPathSegment::Field(FieldId::new(7).expect("field ID is nonzero")),
ValidationPathSegment::ListIndex(3),
])
.expect("fixture path is bounded"),
)
}
#[test]
fn codes_messages_classes_and_recovery_actions_are_stable() {
let expected = [
(
"validation_failed",
"request validation failed",
ErrorClass::InvalidArgument,
RecoveryAction::CorrectRequest,
),
(
"idempotency_key_reuse",
"idempotency key was reused with different input",
ErrorClass::Conflict,
RecoveryAction::CorrectRequest,
),
(
"authorization_denied",
"operation is not authorized",
ErrorClass::PermissionDenied,
RecoveryAction::ObtainPermission,
),
(
"concurrency_deadline_exceeded",
"concurrency deadline exceeded",
ErrorClass::DeadlineExceeded,
RecoveryAction::Retry,
),
(
"contract_mismatch",
"contract version or plan does not match",
ErrorClass::FailedPrecondition,
RecoveryAction::RefreshContract,
),
(
"storage_unavailable",
"storage is temporarily unavailable",
ErrorClass::Unavailable,
RecoveryAction::Retry,
),
(
"outcome_unknown",
"command outcome is not yet known",
ErrorClass::Uncertain,
RecoveryAction::ResolveWithSameIdempotencyKey,
),
(
"internal_defect",
"an internal error occurred",
ErrorClass::Internal,
RecoveryAction::ContactOperator,
),
(
"command_execution_failed",
"command execution failed",
ErrorClass::FailedPrecondition,
RecoveryAction::ContactOperator,
),
(
"history_incarnation_mismatch",
"observed history predates a database restore",
ErrorClass::FailedPrecondition,
RecoveryAction::CorrectRequest,
),
(
"history_pruned",
"requested history has been pruned",
ErrorClass::FailedPrecondition,
RecoveryAction::CorrectRequest,
),
(
"overloaded",
"service is over capacity",
ErrorClass::Unavailable,
RecoveryAction::Retry,
),
(
"follower_mode",
"operation is unavailable in follower mode",
ErrorClass::FailedPrecondition,
RecoveryAction::CorrectRequest,
),
(
"primary_fenced",
"primary is durably fenced",
ErrorClass::FailedPrecondition,
RecoveryAction::ContactOperator,
),
];
assert_eq!(KINDS.len(), 14);
assert_eq!(KINDS[9], PublicErrorKind::HistoryIncarnationMismatch);
assert_eq!(KINDS[10], PublicErrorKind::HistoryPruned);
assert_eq!(KINDS[11], PublicErrorKind::Overloaded);
assert_eq!(APPLICATION_ERROR_CODES.len(), 27);
assert_eq!(
APPLICATION_ERROR_CODES[18],
ApplicationErrorCode::HistoryIncarnationMismatch
);
assert_eq!(
APPLICATION_ERROR_CODES[19],
ApplicationErrorCode::HistoryPruned
);
assert_eq!(
APPLICATION_ERROR_CODES[20],
ApplicationErrorCode::Overloaded
);
assert_eq!(
APPLICATION_ERROR_CODES[21],
ApplicationErrorCode::ProjectionDiverged
);
assert_eq!(
APPLICATION_ERROR_CODES[22],
ApplicationErrorCode::SnapshotRetired
);
assert_eq!(
APPLICATION_ERROR_CODES[23],
ApplicationErrorCode::FreshnessUnsatisfied
);
assert_eq!(
APPLICATION_ERROR_CODES[24],
ApplicationErrorCode::ProjectedSourceRequired
);
for (kind, (code, message, class, recovery_action)) in KINDS.into_iter().zip(expected) {
assert_eq!(kind.code(), code);
assert_eq!(kind.safe_message(), message);
assert_eq!(kind.class(), class);
assert_eq!(kind.recovery_action(), recovery_action);
assert!(code.is_ascii());
assert!(message.is_ascii());
}
}
#[test]
fn every_kind_has_an_authoritative_wire_status_code() {
let expected = [
PublicErrorStatusCode::InvalidArgument,
PublicErrorStatusCode::AlreadyExists,
PublicErrorStatusCode::PermissionDenied,
PublicErrorStatusCode::DeadlineExceeded,
PublicErrorStatusCode::FailedPrecondition,
PublicErrorStatusCode::Unavailable,
PublicErrorStatusCode::Unknown,
PublicErrorStatusCode::Internal,
PublicErrorStatusCode::FailedPrecondition,
PublicErrorStatusCode::FailedPrecondition,
PublicErrorStatusCode::FailedPrecondition,
PublicErrorStatusCode::ResourceExhausted,
PublicErrorStatusCode::FailedPrecondition,
];
for (kind, expected_code) in KINDS.into_iter().zip(expected) {
assert_eq!(kind.status_code(), expected_code);
assert_eq!(PublicError::contextless(kind).status_code(), expected_code);
}
assert_eq!(PublicErrorKind::Overloaded.class(), ErrorClass::Unavailable);
assert_eq!(
PublicErrorKind::Overloaded.status_code(),
PublicErrorStatusCode::ResourceExhausted
);
}
#[test]
fn staged_public_constructors_are_contextless_and_registry_aligned() {
let mismatch = PublicError::history_incarnation_mismatch();
assert_eq!(mismatch.kind(), PublicErrorKind::HistoryIncarnationMismatch);
assert_eq!(mismatch.details(), &PublicErrorDetails::None);
assert_eq!(mismatch.incident_id(), None);
assert_eq!(
ApplicationErrorCode::from_public_kind(mismatch.kind()),
ApplicationErrorCode::HistoryIncarnationMismatch
);
let pruned = PublicError::history_pruned();
assert_eq!(pruned.kind(), PublicErrorKind::HistoryPruned);
assert_eq!(pruned.details(), &PublicErrorDetails::None);
assert_eq!(pruned.incident_id(), None);
assert_eq!(
ApplicationErrorCode::from_public_kind(pruned.kind()),
ApplicationErrorCode::HistoryPruned
);
let overloaded = PublicError::overloaded();
assert_eq!(overloaded.kind(), PublicErrorKind::Overloaded);
assert_eq!(overloaded.details(), &PublicErrorDetails::None);
assert_eq!(overloaded.incident_id(), None);
assert_eq!(
ApplicationErrorCode::from_public_kind(overloaded.kind()),
ApplicationErrorCode::Overloaded
);
}
#[test]
fn concurrency_deadline_error_represents_both_v1_retryable_causes() {
#[derive(Clone, Copy)]
enum Cause {
LogicalConflictDeadline,
ReevaluationBudgetExhausted,
}
const fn public_error_for(cause: Cause) -> PublicError {
match cause {
Cause::LogicalConflictDeadline | Cause::ReevaluationBudgetExhausted => {
PublicError::concurrency_deadline_exceeded()
}
}
}
for cause in [
Cause::LogicalConflictDeadline,
Cause::ReevaluationBudgetExhausted,
] {
let error = public_error_for(cause);
assert_eq!(error.kind(), PublicErrorKind::ConcurrencyDeadlineExceeded);
assert_eq!(error.kind().code(), "concurrency_deadline_exceeded");
assert_eq!(error.kind().safe_message(), "concurrency deadline exceeded");
assert_eq!(error.kind().class(), ErrorClass::DeadlineExceeded);
assert_eq!(error.kind().recovery_action(), RecoveryAction::Retry);
assert_eq!(error.details(), &PublicErrorDetails::None);
}
}
#[test]
fn validation_codes_and_messages_are_stable_and_safe() {
let expected = [
("missing_required_value", "required value is missing"),
("type_mismatch", "value has the wrong type"),
("invalid_value", "value is invalid"),
("out_of_range", "value is outside the allowed range"),
("too_long", "value exceeds the allowed length"),
("too_many_items", "collection has too many items"),
(
"unknown_field",
"field is not defined by the active contract",
),
("duplicate_field", "field occurs more than once"),
];
for (code, (stable_code, stable_message)) in VALIDATION_CODES.into_iter().zip(expected) {
assert_eq!(code.code(), stable_code);
assert_eq!(code.safe_message(), stable_message);
assert!(stable_code.is_ascii());
assert!(stable_message.is_ascii());
}
}
#[test]
fn validation_paths_are_structured_and_bounded() {
let boundary =
vec![
ValidationPathSegment::Field(FieldId::new(1).expect("field ID is nonzero"));
MAX_VALIDATION_PATH_SEGMENTS
];
let path = ValidationPath::new(boundary).expect("boundary is accepted");
assert_eq!(path.segments().len(), MAX_VALIDATION_PATH_SEGMENTS);
assert!(ValidationPath::root().segments().is_empty());
let above_boundary =
vec![ValidationPathSegment::ListIndex(0); MAX_VALIDATION_PATH_SEGMENTS + 1];
assert_eq!(
ValidationPath::new(above_boundary),
Err(ValidationPathError::TooManySegments)
);
}
#[test]
fn validation_issue_collections_are_non_empty_and_bounded() {
assert_eq!(
ValidationIssues::new(Vec::new()),
Err(ValidationIssuesError::Empty)
);
let issues = ValidationIssues::new(vec![issue(); MAX_VALIDATION_ISSUES])
.expect("boundary is accepted");
assert_eq!(issues.as_slice().len(), MAX_VALIDATION_ISSUES);
assert_eq!(
ValidationIssues::new(vec![issue(); MAX_VALIDATION_ISSUES + 1]),
Err(ValidationIssuesError::TooManyIssues)
);
}
#[test]
fn named_constructors_enforce_kind_and_detail_consistency() {
let validation = PublicError::validation(ValidationIssues::one(issue()));
assert_eq!(validation.kind(), PublicErrorKind::Validation);
assert!(matches!(
validation.details(),
PublicErrorDetails::Validation(issues) if issues.as_slice().len() == 1
));
let active_version = ContractVersion::new(41).expect("contract version is nonzero");
let mismatch = PublicError::contract_mismatch(active_version);
assert_eq!(mismatch.kind(), PublicErrorKind::ContractMismatch);
assert_eq!(
mismatch.details(),
&PublicErrorDetails::ContractMismatch {
active_contract_version: active_version,
}
);
let contextless = [
PublicError::idempotency_key_reuse(),
PublicError::authorization_denied(),
PublicError::concurrency_deadline_exceeded(),
PublicError::storage_unavailable(),
PublicError::outcome_unknown(),
];
let expected = [
PublicErrorKind::IdempotencyKeyReuse,
PublicErrorKind::AuthorizationDenied,
PublicErrorKind::ConcurrencyDeadlineExceeded,
PublicErrorKind::StorageUnavailable,
PublicErrorKind::OutcomeUnknown,
];
for (error, kind) in contextless.into_iter().zip(expected) {
assert_eq!(error.kind(), kind);
assert_eq!(error.details(), &PublicErrorDetails::None);
assert_eq!(error.incident_id(), None);
}
let execution =
PublicError::command_execution_failed(ExecutionFailureCode::ArithmeticFault);
assert_eq!(execution.kind(), PublicErrorKind::CommandExecutionFailed);
assert_eq!(
execution.details(),
&PublicErrorDetails::CommandExecutionFailed {
code: ExecutionFailureCode::ArithmeticFault,
}
);
assert_eq!(execution.incident_id(), None);
}
#[test]
fn public_error_never_exposes_a_source() {
let error = PublicError::storage_unavailable();
assert!(error.source().is_none());
}
#[test]
fn internal_conversion_redacts_secret_source_everywhere_public() {
let internal = InternalError::new(incident_id(), DefectScope::Process, SecretSource);
assert!(internal.source().is_some());
assert!(!internal.to_string().contains("secret-canary"));
assert!(!format!("{internal:?}").contains("SecretSource"));
assert_eq!(
internal.source().map(ToString::to_string).as_deref(),
Some("secret-canary-internal-context")
);
let public = PublicError::from(internal);
let display = public.to_string();
let debug = format!("{public:?}");
assert_eq!(public.kind(), PublicErrorKind::InternalDefect);
assert_eq!(public.details(), &PublicErrorDetails::None);
assert_eq!(public.incident_id(), Some(&incident_id()));
assert!(public.source().is_none());
assert!(!display.contains("secret-canary"));
assert!(!debug.contains("secret-canary"));
assert!(!display.contains("SecretSource"));
assert!(!debug.contains("SecretSource"));
}
#[test]
fn validation_details_cannot_carry_secret_canaries() {
let error = PublicError::validation(ValidationIssues::one(issue()));
let display = error.to_string();
let debug = format!("{error:?}");
assert!(!display.contains("secret-canary"));
assert!(!debug.contains("secret-canary"));
assert_eq!(display, "validation_failed: request validation failed");
}
#[test]
fn public_display_contains_only_stable_fields() {
let error = PublicError::internal_defect(incident_id());
assert_eq!(
error.to_string(),
"internal_defect: an internal error occurred (incident 42424242-4242-7242-8242-424242424242)"
);
}
#[test]
fn public_error_details_shapes_carry_no_dynamic_payload_for_secret_fields() {
let error = PublicError::validation(ValidationIssues::one(issue()));
let PublicErrorDetails::Validation(issues) = error.details() else {
panic!("validation details expected");
};
assert_eq!(
issues.as_slice()[0].path().segments()[0],
ValidationPathSegment::Field(FieldId::new(7).expect("nonzero field"))
);
for details in [
PublicErrorDetails::None,
error.details().clone(),
PublicErrorDetails::ContractMismatch {
active_contract_version: ContractVersion::new(3).expect("nonzero version"),
},
PublicErrorDetails::CommandExecutionFailed {
code: ExecutionFailureCode::ArithmeticFault,
},
] {
match details {
PublicErrorDetails::None => {}
PublicErrorDetails::Validation(issues) => {
for issue in issues.as_slice() {
for segment in issue.path().segments() {
match segment {
ValidationPathSegment::Field(_)
| ValidationPathSegment::ListIndex(_) => {}
}
}
}
}
PublicErrorDetails::ContractMismatch {
active_contract_version: _,
} => {}
PublicErrorDetails::CommandExecutionFailed { code: _ } => {}
}
}
}
#[test]
fn incident_source_is_synchronous_fallible_and_has_no_fallback() {
struct FixedSource(IncidentId);
impl IncidentIdSource for FixedSource {
fn next_incident_id(&self) -> Result<IncidentId, IncidentIdSourceError> {
Ok(self.0)
}
}
struct FailingSource;
impl IncidentIdSource for FailingSource {
fn next_incident_id(&self) -> Result<IncidentId, IncidentIdSourceError> {
Err(IncidentIdSourceError)
}
}
assert_eq!(
FixedSource(incident_id()).next_incident_id(),
Ok(incident_id())
);
assert_eq!(FailingSource.next_incident_id(), Err(IncidentIdSourceError));
assert_eq!(
IncidentIdSourceError.to_string(),
"incident identifier source failed"
);
}
#[test]
fn emergency_internal_failure_has_no_identifier_or_disclosive_source() {
let failure = EmergencyInternalFailure::from(IncidentIdSourceError);
assert_eq!(failure.safe_message(), "an internal error occurred");
assert_eq!(failure.class(), ErrorClass::Internal);
assert_eq!(failure.recovery_action(), RecoveryAction::ContactOperator);
assert_eq!(failure.to_string(), "an internal error occurred");
assert_eq!(format!("{failure:?}"), "EmergencyInternalFailure");
assert!(failure.source().is_none());
assert!(!failure.to_string().contains("incident"));
}
}