use std::fmt::{Debug, Display, Formatter};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PeerRpcRemoteErrorCodeV1 {
PayloadTooLarge,
Unauthorized,
Forbidden,
EndpointUnavailable,
TenantMismatch,
ClusterMismatch,
TargetMismatch,
ProtocolMismatch,
Expired,
NonceReplay,
NonceCacheFull,
InvalidBodyHash,
InvalidResponse,
Transport,
InvalidEnvelope,
Unknown,
}
impl PeerRpcRemoteErrorCodeV1 {
pub fn decode(value: Option<&str>) -> Self {
match value {
Some("payload_too_large") => Self::PayloadTooLarge,
Some("unauthorized") => Self::Unauthorized,
Some("forbidden") => Self::Forbidden,
Some("endpoint_unavailable") => Self::EndpointUnavailable,
Some("tenant_mismatch") => Self::TenantMismatch,
Some("cluster_mismatch") => Self::ClusterMismatch,
Some("target_mismatch") => Self::TargetMismatch,
Some("protocol_mismatch") => Self::ProtocolMismatch,
Some("expired") => Self::Expired,
Some("nonce_replay") => Self::NonceReplay,
Some("nonce_cache_full") => Self::NonceCacheFull,
Some("invalid_body_hash") => Self::InvalidBodyHash,
Some("invalid_response") => Self::InvalidResponse,
Some("transport") => Self::Transport,
Some("invalid_envelope") => Self::InvalidEnvelope,
_ => Self::Unknown,
}
}
pub const fn retryable(self) -> bool {
matches!(self, Self::EndpointUnavailable | Self::NonceCacheFull)
}
pub const fn as_str(self) -> &'static str {
match self {
Self::PayloadTooLarge => "payload_too_large",
Self::Unauthorized => "unauthorized",
Self::Forbidden => "forbidden",
Self::EndpointUnavailable => "endpoint_unavailable",
Self::TenantMismatch => "tenant_mismatch",
Self::ClusterMismatch => "cluster_mismatch",
Self::TargetMismatch => "target_mismatch",
Self::ProtocolMismatch => "protocol_mismatch",
Self::Expired => "expired",
Self::NonceReplay => "nonce_replay",
Self::NonceCacheFull => "nonce_cache_full",
Self::InvalidBodyHash => "invalid_body_hash",
Self::InvalidResponse => "invalid_response",
Self::Transport => "transport",
Self::InvalidEnvelope => "invalid_envelope",
Self::Unknown => "unknown",
}
}
}
impl Display for PeerRpcRemoteErrorCodeV1 {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct PeerRpcRemoteErrorV1 {
code: PeerRpcRemoteErrorCodeV1,
correlation_id: String,
}
impl PeerRpcRemoteErrorV1 {
pub fn decode(error: Option<&str>, correlation_id: impl Into<String>) -> Self {
Self {
code: PeerRpcRemoteErrorCodeV1::decode(error),
correlation_id: correlation_id.into(),
}
}
pub const fn code(&self) -> PeerRpcRemoteErrorCodeV1 {
self.code
}
pub fn correlation_id(&self) -> &str {
&self.correlation_id
}
pub const fn retryable(&self) -> bool {
self.code.retryable()
}
}
impl Debug for PeerRpcRemoteErrorV1 {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PeerRpcRemoteErrorV1")
.field("code", &self.code)
.field("has_correlation_id", &!self.correlation_id.is_empty())
.finish()
}
}
impl Display for PeerRpcRemoteErrorV1 {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
write!(formatter, "remote peer rejected request: {}", self.code)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn v1_decoder_uses_exact_codes_and_discards_unknown_input() {
let known = [
(
"payload_too_large",
PeerRpcRemoteErrorCodeV1::PayloadTooLarge,
),
("unauthorized", PeerRpcRemoteErrorCodeV1::Unauthorized),
("forbidden", PeerRpcRemoteErrorCodeV1::Forbidden),
(
"endpoint_unavailable",
PeerRpcRemoteErrorCodeV1::EndpointUnavailable,
),
("tenant_mismatch", PeerRpcRemoteErrorCodeV1::TenantMismatch),
(
"cluster_mismatch",
PeerRpcRemoteErrorCodeV1::ClusterMismatch,
),
("target_mismatch", PeerRpcRemoteErrorCodeV1::TargetMismatch),
(
"protocol_mismatch",
PeerRpcRemoteErrorCodeV1::ProtocolMismatch,
),
("expired", PeerRpcRemoteErrorCodeV1::Expired),
("nonce_replay", PeerRpcRemoteErrorCodeV1::NonceReplay),
("nonce_cache_full", PeerRpcRemoteErrorCodeV1::NonceCacheFull),
(
"invalid_body_hash",
PeerRpcRemoteErrorCodeV1::InvalidBodyHash,
),
(
"invalid_response",
PeerRpcRemoteErrorCodeV1::InvalidResponse,
),
("transport", PeerRpcRemoteErrorCodeV1::Transport),
(
"invalid_envelope",
PeerRpcRemoteErrorCodeV1::InvalidEnvelope,
),
];
for (encoded, expected) in known {
assert_eq!(PeerRpcRemoteErrorCodeV1::decode(Some(encoded)), expected);
}
assert_eq!(
PeerRpcRemoteErrorCodeV1::decode(Some("prefix_endpoint_unavailable_suffix")),
PeerRpcRemoteErrorCodeV1::Unknown
);
assert_eq!(
PeerRpcRemoteErrorCodeV1::decode(None),
PeerRpcRemoteErrorCodeV1::Unknown
);
}
#[test]
fn only_capacity_and_availability_are_retryable_in_v1() {
assert!(PeerRpcRemoteErrorCodeV1::EndpointUnavailable.retryable());
assert!(PeerRpcRemoteErrorCodeV1::NonceCacheFull.retryable());
assert!(!PeerRpcRemoteErrorCodeV1::Unknown.retryable());
assert!(!PeerRpcRemoteErrorCodeV1::Transport.retryable());
}
#[test]
fn debug_omits_correlation_identity() {
let error = PeerRpcRemoteErrorV1::decode(Some("forbidden"), "private-request-marker");
assert!(!format!("{error:?}").contains("private-request-marker"));
}
}