//! RPC error types. /// Status codes for RPC responses. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum RpcStatus { /// Request succeeded. Ok = 0, /// Client sent a malformed request. BadRequest = 1, /// Authentication required or token invalid. Unauthorized = 2, /// Caller lacks permission for this operation. Forbidden = 3, /// Requested resource not found. NotFound = 4, /// Rate limit exceeded. RateLimited = 5, /// Request deadline exceeded (server-side timeout). DeadlineExceeded = 8, /// Server is shutting down (draining). Unavailable = 9, /// Internal server error. Internal = 10, /// Method not recognized. UnknownMethod = 11, } impl RpcStatus { /// Decode a status byte. Returns `None` for unknown values. pub fn from_u8(v: u8) -> Option { match v { 0 => Some(Self::Ok), 1 => Some(Self::BadRequest), 2 => Some(Self::Unauthorized), 3 => Some(Self::Forbidden), 4 => Some(Self::NotFound), 5 => Some(Self::RateLimited), 8 => Some(Self::DeadlineExceeded), 9 => Some(Self::Unavailable), 10 => Some(Self::Internal), 11 => Some(Self::UnknownMethod), _ => None, } } } /// Errors that can occur in the RPC layer. #[derive(Debug, thiserror::Error)] pub enum RpcError { #[error("connection error: {0}")] Connection(String), #[error("encoding error: {0}")] Encode(String), #[error("decoding error: {0}")] Decode(String), #[error("server returned error status {status:?}: {message}")] Server { status: RpcStatus, message: String, }, #[error("request timed out")] Timeout, #[error("stream closed unexpectedly")] StreamClosed, #[error("payload too large: {size} bytes (max {max})")] PayloadTooLarge { size: usize, max: usize }, } impl RpcError { /// Returns `true` if this error is transient and the operation may succeed /// on retry (e.g. connection reset, timeout, server 5xx). Returns `false` /// for permanent failures (auth, bad request, payload limits). pub fn is_retriable(&self) -> bool { match self { Self::Connection(_) | Self::Timeout | Self::StreamClosed => true, Self::Server { status, .. } => matches!( status, RpcStatus::Unavailable | RpcStatus::DeadlineExceeded | RpcStatus::Internal | RpcStatus::RateLimited ), Self::Encode(_) | Self::Decode(_) | Self::PayloadTooLarge { .. } => false, } } } #[cfg(test)] mod tests { use super::*; #[test] fn retriable_errors() { assert!(RpcError::Connection("reset".into()).is_retriable()); assert!(RpcError::Timeout.is_retriable()); assert!(RpcError::StreamClosed.is_retriable()); assert!(RpcError::Server { status: RpcStatus::Unavailable, message: String::new(), } .is_retriable()); assert!(RpcError::Server { status: RpcStatus::Internal, message: String::new(), } .is_retriable()); assert!(RpcError::Server { status: RpcStatus::DeadlineExceeded, message: String::new(), } .is_retriable()); assert!(RpcError::Server { status: RpcStatus::RateLimited, message: String::new(), } .is_retriable()); } #[test] fn non_retriable_errors() { assert!(!RpcError::Encode("bad".into()).is_retriable()); assert!(!RpcError::Decode("bad".into()).is_retriable()); assert!(!RpcError::PayloadTooLarge { size: 100, max: 50 }.is_retriable()); assert!(!RpcError::Server { status: RpcStatus::Unauthorized, message: String::new(), } .is_retriable()); assert!(!RpcError::Server { status: RpcStatus::BadRequest, message: String::new(), } .is_retriable()); assert!(!RpcError::Server { status: RpcStatus::Forbidden, message: String::new(), } .is_retriable()); assert!(!RpcError::Server { status: RpcStatus::NotFound, message: String::new(), } .is_retriable()); } #[test] fn connection_state_display() { use crate::client::ConnectionState; assert_eq!(ConnectionState::Connected.to_string(), "Connected"); assert_eq!(ConnectionState::Disconnected.to_string(), "Disconnected"); assert_eq!( ConnectionState::Reconnecting { attempt: 2 }.to_string(), "Reconnecting (attempt 2)" ); } }