Skip to main content

riffdb_client_rust/
status.rs

1//! Fail-closed validation of public gRPC failures.
2
3use std::error::Error;
4use std::fmt;
5
6use riffdb_errors::{
7    ApplicationError, ApplicationErrorCode, MAX_APPLICATION_ERROR_BYTES, PublicError,
8    PublicErrorKind, PublicErrorStatusCode,
9};
10use riffdb_proto::{
11    ApplicationErrorWireError, MAX_PUBLIC_ERROR_BYTES, PublicErrorWireError,
12    decode_application_error, decode_public_error,
13};
14use tonic::{Code, Status};
15
16use crate::ids::IdentifierGenerationError;
17
18const AUTHENTICATION_FAILED: &str = "authentication failed";
19const REQUEST_CANCELLED: &str = "request was cancelled";
20const REQUEST_DEADLINE_ELAPSED: &str = "request deadline elapsed";
21const RESPONSE_TOO_LARGE: &str = "response exceeds the service limit";
22const EMERGENCY_INTERNAL: &str = "an internal error occurred";
23
24/// A valid details-free status from a closed non-`PublicError` boundary.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum DetailsFreeStatus {
27    /// Authentication failed before an API-neutral request context existed.
28    Unauthenticated,
29    /// Cancellation was proven before the applicable admission boundary.
30    Cancelled,
31    /// The request deadline elapsed before a protected result was released.
32    DeadlineExceeded,
33    /// One complete service result exceeded the response budget.
34    ResponseTooLarge,
35    /// Internal containment could not obtain a real incident identifier.
36    EmergencyInternal,
37    /// The transport became unavailable without a structured server result.
38    TransportUnavailable,
39}
40
41/// The closed reason an untrusted gRPC failure was rejected.
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum ProtocolFailureKind {
44    /// A status that requires `PublicError` detail carried none.
45    MissingPublicErrorDetails,
46    /// Structured detail exceeded the exact 16 KiB ceiling.
47    OversizedPublicErrorDetails,
48    /// Structured detail was not valid Protobuf.
49    MalformedPublicErrorDetails,
50    /// Structured detail named an unknown public-error kind.
51    UnknownPublicErrorKind,
52    /// Structured detail violated its closed field/detail invariants.
53    InvalidPublicErrorDetails,
54    /// The gRPC code disagreed with the checked public error class.
55    StatusCodeMismatch,
56    /// `grpc-message` disagreed with the checked registry-owned safe message.
57    StatusMessageMismatch,
58    /// A details-free closed status carried an unrecognized code/message pair.
59    InvalidDetailsFreeStatus,
60    /// A request built by the caller failed public structural validation.
61    InvalidOutboundMessage,
62    /// A successful response or stream item failed public structural validation.
63    InvalidInboundMessage,
64    /// An application RPC carried no application error details.
65    MissingApplicationErrorDetails,
66    /// Application error details exceeded their exact ceiling.
67    OversizedApplicationErrorDetails,
68    /// Application error details were malformed or noncanonical.
69    InvalidApplicationErrorDetails,
70    /// The application status code disagreed with its checked semantic code.
71    ApplicationStatusCodeMismatch,
72    /// The application status message disagreed with its registry-owned message.
73    ApplicationStatusMessageMismatch,
74}
75
76/// A bounded protocol failure containing no peer-provided text or bytes.
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub struct ProtocolFailure {
79    kind: ProtocolFailureKind,
80}
81
82impl ProtocolFailure {
83    pub(crate) const fn new(kind: ProtocolFailureKind) -> Self {
84        Self { kind }
85    }
86
87    /// Returns the closed validation failure kind.
88    #[must_use]
89    pub const fn kind(self) -> ProtocolFailureKind {
90        self.kind
91    }
92}
93
94impl fmt::Display for ProtocolFailure {
95    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96        formatter.write_str("the RiffDB gRPC peer returned an invalid protocol response")
97    }
98}
99
100impl Error for ProtocolFailure {}
101
102/// The public client's explicit unresolved idempotent-operation disposition.
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104pub struct OutcomeUnknown;
105
106impl fmt::Display for OutcomeUnknown {
107    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108        formatter.write_str("the command outcome remains unknown")
109    }
110}
111
112impl Error for OutcomeUnknown {}
113
114/// A closed failure returned by the Rust SDK.
115#[derive(Debug)]
116pub enum ClientError {
117    /// A fully checked public-safe RiffDB failure.
118    Public(PublicError),
119    /// A fully checked symbolic application failure.
120    Application(Box<ApplicationError>),
121    /// A valid status from a closed details-free boundary.
122    DetailsFree(DetailsFreeStatus),
123    /// The peer violated the public gRPC contract.
124    Protocol(ProtocolFailure),
125    /// A local UUIDv7 source failed before the next submission.
126    IdentifierGeneration(IdentifierGenerationError),
127    /// Channel construction or connection failed before a typed response existed.
128    ConnectionFailure,
129    /// Verified TLS configuration, trust-root, connection, or peer failure.
130    Tls(crate::TlsClientFailure),
131    /// Automatic same-input recovery exhausted its explicit attempt budget.
132    OutcomeUnknown(OutcomeUnknown),
133}
134
135impl ClientError {
136    /// Validates an untrusted non-OK gRPC status without trusting its message.
137    #[must_use]
138    pub fn from_status(status: Status) -> Self {
139        checked_status(status)
140    }
141
142    /// Returns a checked public error, when this is one.
143    #[must_use]
144    pub const fn public_error(&self) -> Option<&PublicError> {
145        match self {
146            Self::Public(error) => Some(error),
147            Self::Application(_)
148            | Self::DetailsFree(_)
149            | Self::Protocol(_)
150            | Self::IdentifierGeneration(_)
151            | Self::ConnectionFailure
152            | Self::Tls(_)
153            | Self::OutcomeUnknown(_) => None,
154        }
155    }
156
157    /// Returns a checked symbolic application failure, when this is one.
158    #[must_use]
159    pub const fn application_error(&self) -> Option<&ApplicationError> {
160        match self {
161            Self::Application(error) => Some(error),
162            Self::Public(_)
163            | Self::DetailsFree(_)
164            | Self::Protocol(_)
165            | Self::IdentifierGeneration(_)
166            | Self::ConnectionFailure
167            | Self::Tls(_)
168            | Self::OutcomeUnknown(_) => None,
169        }
170    }
171}
172
173impl fmt::Display for ClientError {
174    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
175        match self {
176            Self::Public(error) => error.fmt(formatter),
177            Self::Application(error) => error.fmt(formatter),
178            Self::DetailsFree(status) => formatter.write_str(match status {
179                DetailsFreeStatus::Unauthenticated => AUTHENTICATION_FAILED,
180                DetailsFreeStatus::Cancelled => REQUEST_CANCELLED,
181                DetailsFreeStatus::DeadlineExceeded => REQUEST_DEADLINE_ELAPSED,
182                DetailsFreeStatus::ResponseTooLarge => RESPONSE_TOO_LARGE,
183                DetailsFreeStatus::EmergencyInternal => EMERGENCY_INTERNAL,
184                DetailsFreeStatus::TransportUnavailable => "the gRPC transport is unavailable",
185            }),
186            Self::Protocol(error) => error.fmt(formatter),
187            Self::IdentifierGeneration(error) => error.fmt(formatter),
188            Self::ConnectionFailure => formatter.write_str("the gRPC channel could not connect"),
189            Self::Tls(error) => error.fmt(formatter),
190            Self::OutcomeUnknown(error) => error.fmt(formatter),
191        }
192    }
193}
194
195impl Error for ClientError {}
196
197pub(crate) fn checked_status(status: Status) -> ClientError {
198    let details = status.details();
199    if details.is_empty() {
200        return checked_details_free_status(
201            status.code(),
202            status.message(),
203            Error::source(&status).is_some(),
204        );
205    }
206    if details.len() > MAX_PUBLIC_ERROR_BYTES {
207        return protocol(ProtocolFailureKind::OversizedPublicErrorDetails);
208    }
209
210    let error = match decode_public_error(details) {
211        Ok(error) => error,
212        Err(error) => return protocol(map_wire_error(error)),
213    };
214    if status.code() != code_for_public_error(&error) {
215        return protocol(ProtocolFailureKind::StatusCodeMismatch);
216    }
217    if status.message() != error.safe_message() {
218        return protocol(ProtocolFailureKind::StatusMessageMismatch);
219    }
220    ClientError::Public(error)
221}
222
223pub(crate) fn checked_application_status(status: Status) -> ClientError {
224    let details = status.details();
225    if details.is_empty() {
226        let checked = checked_details_free_status(
227            status.code(),
228            status.message(),
229            Error::source(&status).is_some(),
230        );
231        return match checked {
232            ClientError::DetailsFree(DetailsFreeStatus::EmergencyInternal)
233            | ClientError::DetailsFree(DetailsFreeStatus::TransportUnavailable) => checked,
234            _ => protocol(ProtocolFailureKind::MissingApplicationErrorDetails),
235        };
236    }
237    if details.len() > MAX_APPLICATION_ERROR_BYTES {
238        return protocol(ProtocolFailureKind::OversizedApplicationErrorDetails);
239    }
240    let error = match decode_application_error(details) {
241        Ok(error) => error,
242        Err(error) => return protocol(map_application_wire_error(error)),
243    };
244    if status.code() != code_for_application(error.code()) {
245        return protocol(ProtocolFailureKind::ApplicationStatusCodeMismatch);
246    }
247    if status.message() != error.safe_message() {
248        return protocol(ProtocolFailureKind::ApplicationStatusMessageMismatch);
249    }
250    ClientError::Application(Box::new(error))
251}
252
253fn checked_details_free_status(code: Code, message: &str, has_local_source: bool) -> ClientError {
254    let status = match (code, message) {
255        (Code::Unauthenticated, AUTHENTICATION_FAILED) => DetailsFreeStatus::Unauthenticated,
256        (Code::Cancelled, REQUEST_CANCELLED) => DetailsFreeStatus::Cancelled,
257        (Code::DeadlineExceeded, REQUEST_DEADLINE_ELAPSED) => DetailsFreeStatus::DeadlineExceeded,
258        (Code::ResourceExhausted, RESPONSE_TOO_LARGE) => DetailsFreeStatus::ResponseTooLarge,
259        (Code::Internal, EMERGENCY_INTERNAL) => DetailsFreeStatus::EmergencyInternal,
260        // Tonic attaches a source only to locally synthesized transport errors.
261        // H2 may surface an incomplete response as INTERNAL rather than
262        // UNAVAILABLE. A peer-originated status has no local source and still
263        // fails closed below.
264        (_, _) if has_local_source => DetailsFreeStatus::TransportUnavailable,
265        (
266            _,
267            AUTHENTICATION_FAILED
268            | REQUEST_CANCELLED
269            | REQUEST_DEADLINE_ELAPSED
270            | RESPONSE_TOO_LARGE
271            | EMERGENCY_INTERNAL,
272        ) => {
273            return protocol(ProtocolFailureKind::InvalidDetailsFreeStatus);
274        }
275        _ => return protocol(ProtocolFailureKind::MissingPublicErrorDetails),
276    };
277    ClientError::DetailsFree(status)
278}
279
280const fn code_for_public_error(error: &PublicError) -> Code {
281    tonic_code(error.status_code())
282}
283
284const fn tonic_code(code: PublicErrorStatusCode) -> Code {
285    match code {
286        PublicErrorStatusCode::InvalidArgument => Code::InvalidArgument,
287        PublicErrorStatusCode::AlreadyExists => Code::AlreadyExists,
288        PublicErrorStatusCode::PermissionDenied => Code::PermissionDenied,
289        PublicErrorStatusCode::DeadlineExceeded => Code::DeadlineExceeded,
290        PublicErrorStatusCode::FailedPrecondition => Code::FailedPrecondition,
291        PublicErrorStatusCode::ResourceExhausted => Code::ResourceExhausted,
292        PublicErrorStatusCode::Unavailable => Code::Unavailable,
293        PublicErrorStatusCode::Unknown => Code::Unknown,
294        PublicErrorStatusCode::Internal => Code::Internal,
295    }
296}
297
298const fn code_for_application(code: ApplicationErrorCode) -> Code {
299    match code {
300        ApplicationErrorCode::InvalidRequest
301        | ApplicationErrorCode::InputInvalid
302        | ApplicationErrorCode::QueryInvalid
303        | ApplicationErrorCode::CursorInvalid => Code::InvalidArgument,
304        ApplicationErrorCode::AuthorizationDenied | ApplicationErrorCode::CapabilityRevoked => {
305            Code::PermissionDenied
306        }
307        ApplicationErrorCode::ContractMismatch
308        | ApplicationErrorCode::QueryUnavailable
309        | ApplicationErrorCode::ModuleUnavailable
310        | ApplicationErrorCode::CommandExecutionFailed
311        | ApplicationErrorCode::HistoryIncarnationMismatch
312        | ApplicationErrorCode::FollowerMode
313        | ApplicationErrorCode::PrimaryFenced
314        | ApplicationErrorCode::HistoryPruned
315        | ApplicationErrorCode::ProjectionDiverged
316        | ApplicationErrorCode::SnapshotRetired
317        | ApplicationErrorCode::FreshnessUnsatisfied
318        | ApplicationErrorCode::ProjectedSourceRequired => Code::FailedPrecondition,
319        ApplicationErrorCode::ResponseTooLarge | ApplicationErrorCode::Overloaded => {
320            Code::ResourceExhausted
321        }
322        ApplicationErrorCode::StorageUnavailable => Code::Unavailable,
323        ApplicationErrorCode::OutcomeUnknown => Code::Unknown,
324        ApplicationErrorCode::OperationCancelled => Code::Cancelled,
325        ApplicationErrorCode::DeadlineExceeded => Code::DeadlineExceeded,
326        ApplicationErrorCode::InternalDefect | ApplicationErrorCode::ProtocolInvalid => {
327            Code::Internal
328        }
329        ApplicationErrorCode::IdempotencyKeyReuse => Code::AlreadyExists,
330    }
331}
332
333const fn map_wire_error(error: PublicErrorWireError) -> ProtocolFailureKind {
334    match error {
335        PublicErrorWireError::MessageTooLarge => ProtocolFailureKind::OversizedPublicErrorDetails,
336        PublicErrorWireError::MalformedEncoding | PublicErrorWireError::PreflightLimitExceeded => {
337            ProtocolFailureKind::MalformedPublicErrorDetails
338        }
339        PublicErrorWireError::UnknownKind => ProtocolFailureKind::UnknownPublicErrorKind,
340        PublicErrorWireError::InconsistentBoundary
341        | PublicErrorWireError::InvalidIncidentId
342        | PublicErrorWireError::MissingInternalIncident
343        | PublicErrorWireError::InvalidValidationDetails
344        | PublicErrorWireError::InvalidContractMismatchDetails
345        | PublicErrorWireError::InvalidExecutionFailureDetails => {
346            ProtocolFailureKind::InvalidPublicErrorDetails
347        }
348    }
349}
350
351const fn map_application_wire_error(error: ApplicationErrorWireError) -> ProtocolFailureKind {
352    match error {
353        ApplicationErrorWireError::MessageTooLarge => {
354            ProtocolFailureKind::OversizedApplicationErrorDetails
355        }
356        ApplicationErrorWireError::MalformedEncoding
357        | ApplicationErrorWireError::PreflightLimitExceeded
358        | ApplicationErrorWireError::UnknownField
359        | ApplicationErrorWireError::DuplicateField
360        | ApplicationErrorWireError::UnknownCode
361        | ApplicationErrorWireError::UnknownOperation
362        | ApplicationErrorWireError::UnknownFix
363        | ApplicationErrorWireError::InconsistentRegistry
364        | ApplicationErrorWireError::InvalidContext
365        | ApplicationErrorWireError::MissingInternalIncident => {
366            ProtocolFailureKind::InvalidApplicationErrorDetails
367        }
368    }
369}
370
371const fn protocol(kind: ProtocolFailureKind) -> ClientError {
372    ClientError::Protocol(ProtocolFailure::new(kind))
373}
374
375pub(crate) const fn is_retryable(error: &ClientError) -> bool {
376    match error {
377        ClientError::Public(error) => matches!(
378            error.recovery_action(),
379            riffdb_errors::RecoveryAction::Retry
380                | riffdb_errors::RecoveryAction::ResolveWithSameIdempotencyKey
381        ),
382        ClientError::Application(error) => matches!(
383            error.recovery_action(),
384            riffdb_errors::ApplicationRecoveryAction::Retry
385                | riffdb_errors::ApplicationRecoveryAction::ResolveWithSameIdempotencyKey
386        ),
387        ClientError::DetailsFree(DetailsFreeStatus::TransportUnavailable) => true,
388        ClientError::DetailsFree(_)
389        | ClientError::Protocol(_)
390        | ClientError::IdentifierGeneration(_)
391        | ClientError::ConnectionFailure
392        | ClientError::Tls(_)
393        | ClientError::OutcomeUnknown(_) => false,
394    }
395}
396
397pub(crate) const fn carries_uncertainty(error: &ClientError) -> bool {
398    matches!(
399        error,
400        ClientError::Public(error) if matches!(error.kind(), PublicErrorKind::OutcomeUnknown)
401    ) || matches!(
402        error,
403        ClientError::Application(error)
404            if matches!(error.code(), ApplicationErrorCode::OutcomeUnknown)
405    ) || matches!(
406        error,
407        ClientError::DetailsFree(DetailsFreeStatus::TransportUnavailable)
408            | ClientError::OutcomeUnknown(_)
409    )
410}
411
412#[cfg(test)]
413mod tests {
414    use std::sync::Arc;
415
416    use riffdb_errors::ErrorClass;
417
418    use super::*;
419
420    #[derive(Debug)]
421    struct LocalTransportFailure;
422
423    impl fmt::Display for LocalTransportFailure {
424        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
425            formatter.write_str("local channel failure")
426        }
427    }
428
429    impl Error for LocalTransportFailure {}
430
431    #[test]
432    fn details_free_boundaries_require_exact_code_and_static_message() {
433        assert!(matches!(
434            checked_status(Status::new(Code::Unauthenticated, AUTHENTICATION_FAILED)),
435            ClientError::DetailsFree(DetailsFreeStatus::Unauthenticated)
436        ));
437        assert!(matches!(
438            checked_status(Status::new(Code::ResourceExhausted, RESPONSE_TOO_LARGE)),
439            ClientError::DetailsFree(DetailsFreeStatus::ResponseTooLarge)
440        ));
441        assert!(matches!(
442            checked_status(Status::new(Code::Internal, RESPONSE_TOO_LARGE)),
443            ClientError::Protocol(ProtocolFailure {
444                kind: ProtocolFailureKind::InvalidDetailsFreeStatus
445            })
446        ));
447    }
448
449    #[test]
450    fn source_free_unavailable_without_public_error_details_fails_closed() {
451        let canary = "peer supplied diagnostic secret";
452        let error = checked_status(Status::new(Code::Unavailable, canary));
453        assert!(!error.to_string().contains(canary));
454        assert!(!is_retryable(&error));
455        assert!(!carries_uncertainty(&error));
456        assert_protocol_kind(error, ProtocolFailureKind::MissingPublicErrorDetails);
457    }
458
459    #[test]
460    fn source_bearing_local_unavailable_is_transport_loss_without_trusting_message() {
461        let canary = "local diagnostic secret";
462        let mut status = Status::new(Code::Unavailable, canary);
463        status.set_source(Arc::new(LocalTransportFailure));
464        let error = checked_status(status);
465        assert!(matches!(
466            &error,
467            ClientError::DetailsFree(DetailsFreeStatus::TransportUnavailable)
468        ));
469        assert!(is_retryable(&error));
470        assert!(carries_uncertainty(&error));
471        assert!(!error.to_string().contains(canary));
472    }
473
474    #[test]
475    fn source_bearing_local_h2_protocol_failure_is_transport_loss() {
476        let canary = "local h2 response-frame diagnostic";
477        let mut status = Status::new(Code::Internal, canary);
478        status.set_source(Arc::new(LocalTransportFailure));
479        let error = checked_status(status);
480        assert!(matches!(
481            &error,
482            ClientError::DetailsFree(DetailsFreeStatus::TransportUnavailable)
483        ));
484        assert!(is_retryable(&error));
485        assert!(carries_uncertainty(&error));
486        assert!(!error.to_string().contains(canary));
487
488        let source_free = checked_status(Status::new(Code::Internal, canary));
489        assert_protocol_kind(source_free, ProtocolFailureKind::MissingPublicErrorDetails);
490    }
491
492    #[test]
493    fn missing_malformed_and_oversized_details_fail_closed() {
494        assert_protocol_kind(
495            checked_status(Status::new(Code::PermissionDenied, "authorization denied")),
496            ProtocolFailureKind::MissingPublicErrorDetails,
497        );
498        assert_protocol_kind(
499            checked_status(Status::with_details(
500                Code::PermissionDenied,
501                "authorization denied",
502                vec![0xff].into(),
503            )),
504            ProtocolFailureKind::MalformedPublicErrorDetails,
505        );
506        assert_protocol_kind(
507            checked_status(Status::with_details(
508                Code::PermissionDenied,
509                "authorization denied",
510                vec![0_u8; MAX_PUBLIC_ERROR_BYTES + 1].into(),
511            )),
512            ProtocolFailureKind::OversizedPublicErrorDetails,
513        );
514    }
515
516    #[test]
517    fn checked_public_error_requires_consistent_status_and_message() {
518        let details = authorization_denied_bytes();
519        let checked = checked_status(Status::with_details(
520            Code::PermissionDenied,
521            "operation is not authorized",
522            details.clone().into(),
523        ));
524        assert!(matches!(
525            checked,
526            ClientError::Public(ref error)
527                if error.kind() == PublicErrorKind::AuthorizationDenied
528        ));
529
530        assert_protocol_kind(
531            checked_status(Status::with_details(
532                Code::InvalidArgument,
533                "operation is not authorized",
534                details.clone().into(),
535            )),
536            ProtocolFailureKind::StatusCodeMismatch,
537        );
538        assert_protocol_kind(
539            checked_status(Status::with_details(
540                Code::PermissionDenied,
541                "caller supplied text",
542                details.into(),
543            )),
544            ProtocolFailureKind::StatusMessageMismatch,
545        );
546    }
547
548    #[test]
549    fn checked_application_error_rejects_downgrade_status_and_message_drift() {
550        let application = ApplicationError::new(
551            ApplicationErrorCode::AuthorizationDenied,
552            riffdb_errors::ApplicationOperation::ExecuteQuery,
553            riffdb_errors::ApplicationErrorContext::empty(),
554            None,
555        );
556        let details = riffdb_proto::encode_application_error(&application);
557        let checked = checked_application_status(Status::with_details(
558            Code::PermissionDenied,
559            application.safe_message(),
560            details.clone().into(),
561        ));
562        assert!(matches!(
563            checked,
564            ClientError::Application(ref error) if error.as_ref() == &application
565        ));
566        assert_protocol_kind(
567            checked_application_status(Status::with_details(
568                Code::InvalidArgument,
569                application.safe_message(),
570                details.clone().into(),
571            )),
572            ProtocolFailureKind::ApplicationStatusCodeMismatch,
573        );
574        assert_protocol_kind(
575            checked_application_status(Status::with_details(
576                Code::PermissionDenied,
577                "peer supplied authorization detail",
578                details.into(),
579            )),
580            ProtocolFailureKind::ApplicationStatusMessageMismatch,
581        );
582
583        let legacy_kernel_details = authorization_denied_bytes();
584        assert_protocol_kind(
585            checked_application_status(Status::with_details(
586                Code::PermissionDenied,
587                "operation is not authorized",
588                legacy_kernel_details.into(),
589            )),
590            ProtocolFailureKind::InvalidApplicationErrorDetails,
591        );
592    }
593
594    #[test]
595    fn concurrency_deadline_status_preserves_the_complete_retry_contract() {
596        let details = contextless_error_bytes(
597            4,
598            "concurrency_deadline_exceeded",
599            "concurrency deadline exceeded",
600            2,
601        );
602        let checked = checked_status(Status::with_details(
603            Code::DeadlineExceeded,
604            "concurrency deadline exceeded",
605            details.into(),
606        ));
607        let ClientError::Public(error) = checked else {
608            panic!("checked public error")
609        };
610        assert_eq!(error.kind(), PublicErrorKind::ConcurrencyDeadlineExceeded);
611        assert_eq!(error.code(), "concurrency_deadline_exceeded");
612        assert_eq!(error.safe_message(), "concurrency deadline exceeded");
613        assert_eq!(
614            error.recovery_action(),
615            riffdb_errors::RecoveryAction::Retry
616        );
617        assert_eq!(error.class(), ErrorClass::DeadlineExceeded);
618    }
619
620    #[test]
621    fn unknown_public_error_kind_never_becomes_retryable() {
622        let details = contextless_error_bytes(99, "future", "future", 2);
623        assert_protocol_kind(
624            checked_status(Status::with_details(
625                Code::Unavailable,
626                "future",
627                details.into(),
628            )),
629            ProtocolFailureKind::UnknownPublicErrorKind,
630        );
631    }
632
633    #[test]
634    fn staged_public_errors_preserve_retryability_and_certainty() {
635        let overloaded = ClientError::Public(PublicError::overloaded());
636        assert!(is_retryable(&overloaded));
637        assert!(!carries_uncertainty(&overloaded));
638
639        let mismatch = ClientError::Public(PublicError::history_incarnation_mismatch());
640        assert!(!is_retryable(&mismatch));
641        assert!(!carries_uncertainty(&mismatch));
642
643        let pruned = ClientError::Public(PublicError::history_pruned());
644        assert!(!is_retryable(&pruned));
645        assert!(!carries_uncertainty(&pruned));
646
647        let overloaded_app = ClientError::Application(Box::new(ApplicationError::new(
648            ApplicationErrorCode::Overloaded,
649            riffdb_errors::ApplicationOperation::ExecuteQuery,
650            riffdb_errors::ApplicationErrorContext::empty(),
651            None,
652        )));
653        assert!(is_retryable(&overloaded_app));
654        assert!(!carries_uncertainty(&overloaded_app));
655
656        let mismatch_app = ClientError::Application(Box::new(ApplicationError::new(
657            ApplicationErrorCode::HistoryIncarnationMismatch,
658            riffdb_errors::ApplicationOperation::ExecuteQuery,
659            riffdb_errors::ApplicationErrorContext::empty(),
660            None,
661        )));
662        assert!(!is_retryable(&mismatch_app));
663        assert!(!carries_uncertainty(&mismatch_app));
664
665        let pruned_app = ClientError::Application(Box::new(ApplicationError::new(
666            ApplicationErrorCode::HistoryPruned,
667            riffdb_errors::ApplicationOperation::ExecuteQuery,
668            riffdb_errors::ApplicationErrorContext::empty(),
669            None,
670        )));
671        assert!(!is_retryable(&pruned_app));
672        assert!(!carries_uncertainty(&pruned_app));
673    }
674
675    #[test]
676    fn staged_public_status_codes_accept_kind_level_overrides() {
677        let overloaded_details =
678            contextless_error_bytes(11, "overloaded", "service is over capacity", 2);
679        let checked = checked_status(Status::with_details(
680            Code::ResourceExhausted,
681            "service is over capacity",
682            overloaded_details.into(),
683        ));
684        assert!(matches!(
685            checked,
686            ClientError::Public(ref error) if error.kind() == PublicErrorKind::Overloaded
687        ));
688
689        let mismatch_details = contextless_error_bytes(
690            10,
691            "history_incarnation_mismatch",
692            "observed history predates a database restore",
693            1,
694        );
695        let checked = checked_status(Status::with_details(
696            Code::FailedPrecondition,
697            "observed history predates a database restore",
698            mismatch_details.into(),
699        ));
700        assert!(matches!(
701            checked,
702            ClientError::Public(ref error)
703                if error.kind() == PublicErrorKind::HistoryIncarnationMismatch
704        ));
705
706        let pruned_details =
707            contextless_error_bytes(12, "history_pruned", "requested history has been pruned", 1);
708        let checked = checked_status(Status::with_details(
709            Code::FailedPrecondition,
710            "requested history has been pruned",
711            pruned_details.into(),
712        ));
713        assert!(matches!(
714            checked,
715            ClientError::Public(ref error) if error.kind() == PublicErrorKind::HistoryPruned
716        ));
717    }
718
719    fn authorization_denied_bytes() -> Vec<u8> {
720        contextless_error_bytes(3, "authorization_denied", "operation is not authorized", 4)
721    }
722
723    fn contextless_error_bytes(
724        kind: u8,
725        code: &str,
726        safe_message: &str,
727        recovery_action: u8,
728    ) -> Vec<u8> {
729        let mut bytes = Vec::new();
730        push_varint_field(&mut bytes, 1, kind);
731        push_string_field(&mut bytes, 2, code);
732        push_string_field(&mut bytes, 3, safe_message);
733        push_varint_field(&mut bytes, 4, recovery_action);
734        bytes
735    }
736
737    fn push_varint_field(output: &mut Vec<u8>, field: u8, value: u8) {
738        output.push(field << 3);
739        output.push(value);
740    }
741
742    fn push_string_field(output: &mut Vec<u8>, field: u8, value: &str) {
743        output.push((field << 3) | 2);
744        output.push(u8::try_from(value.len()).expect("short fixture"));
745        output.extend_from_slice(value.as_bytes());
746    }
747
748    fn assert_protocol_kind(error: ClientError, expected: ProtocolFailureKind) {
749        assert!(matches!(
750            error,
751            ClientError::Protocol(failure) if failure.kind() == expected
752        ));
753    }
754}