1use 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum DetailsFreeStatus {
27 Unauthenticated,
29 Cancelled,
31 DeadlineExceeded,
33 ResponseTooLarge,
35 EmergencyInternal,
37 TransportUnavailable,
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum ProtocolFailureKind {
44 MissingPublicErrorDetails,
46 OversizedPublicErrorDetails,
48 MalformedPublicErrorDetails,
50 UnknownPublicErrorKind,
52 InvalidPublicErrorDetails,
54 StatusCodeMismatch,
56 StatusMessageMismatch,
58 InvalidDetailsFreeStatus,
60 InvalidOutboundMessage,
62 InvalidInboundMessage,
64 MissingApplicationErrorDetails,
66 OversizedApplicationErrorDetails,
68 InvalidApplicationErrorDetails,
70 ApplicationStatusCodeMismatch,
72 ApplicationStatusMessageMismatch,
74}
75
76#[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 #[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#[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#[derive(Debug)]
116pub enum ClientError {
117 Public(PublicError),
119 Application(Box<ApplicationError>),
121 DetailsFree(DetailsFreeStatus),
123 Protocol(ProtocolFailure),
125 IdentifierGeneration(IdentifierGenerationError),
127 ConnectionFailure,
129 Tls(crate::TlsClientFailure),
131 OutcomeUnknown(OutcomeUnknown),
133}
134
135impl ClientError {
136 #[must_use]
138 pub fn from_status(status: Status) -> Self {
139 checked_status(status)
140 }
141
142 #[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 #[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 (_, _) 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}