use std::{error, fmt, io, sync::Arc};
use arcstr::ArcStr;
use crate::{
ParsingError,
errors::server_error::{ServerError, ServerErrorKind},
};
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[non_exhaustive]
pub enum ErrorKind {
Parse,
AuthenticationFailed,
UnexpectedReturnType,
InvalidClientConfig,
Io,
Client,
Extension,
MasterNameNotFoundBySentinel,
NoValidReplicasFoundBySentinel,
EmptySentinelList,
ClusterConnectionNotFound,
Server(ServerErrorKind),
#[cfg(feature = "json")]
Serialize,
RESP3NotSupported,
}
#[derive(Clone)]
pub struct RedisError {
repr: ErrorRepr,
}
#[cfg(feature = "json")]
impl From<serde_json::Error> for RedisError {
fn from(serde_err: serde_json::Error) -> RedisError {
RedisError {
repr: ErrorRepr::Internal {
kind: ErrorKind::Serialize,
err: Arc::new(serde_err),
},
}
}
}
#[derive(Debug, Clone)]
enum ErrorRepr {
General(ErrorKind, &'static str, Option<ArcStr>),
Internal {
kind: ErrorKind,
err: Arc<dyn error::Error + Send + Sync>,
},
Parsing(ParsingError),
Server(ServerError),
Pipeline(Arc<[(usize, ServerError)]>),
TransactionAborted(Arc<[(usize, ServerError)]>),
}
impl PartialEq for RedisError {
fn eq(&self, other: &RedisError) -> bool {
match (&self.repr, &other.repr) {
(&ErrorRepr::General(kind_a, _, _), &ErrorRepr::General(kind_b, _, _)) => {
kind_a == kind_b
}
(ErrorRepr::Parsing(a), ErrorRepr::Parsing(b)) => *a == *b,
(ErrorRepr::Server(a), ErrorRepr::Server(b)) => *a == *b,
(ErrorRepr::Pipeline(a), ErrorRepr::Pipeline(b)) => *a == *b,
_ => false,
}
}
}
impl From<io::Error> for RedisError {
fn from(err: io::Error) -> RedisError {
RedisError {
repr: ErrorRepr::Internal {
kind: ErrorKind::Io,
err: Arc::new(err),
},
}
}
}
#[cfg(feature = "tls-rustls")]
impl From<rustls::pki_types::InvalidDnsNameError> for RedisError {
fn from(err: rustls::pki_types::InvalidDnsNameError) -> RedisError {
RedisError {
repr: ErrorRepr::Internal {
kind: ErrorKind::Io,
err: Arc::new(err),
},
}
}
}
#[cfg(feature = "tls-rustls")]
impl From<rustls_native_certs::Error> for RedisError {
fn from(err: rustls_native_certs::Error) -> RedisError {
RedisError {
repr: ErrorRepr::Internal {
kind: ErrorKind::Io,
err: Arc::new(err),
},
}
}
}
impl From<(ErrorKind, &'static str)> for RedisError {
fn from((kind, desc): (ErrorKind, &'static str)) -> RedisError {
RedisError {
repr: ErrorRepr::General(kind, desc, None),
}
}
}
impl From<(ErrorKind, &'static str, String)> for RedisError {
fn from((kind, desc, detail): (ErrorKind, &'static str, String)) -> RedisError {
RedisError {
repr: ErrorRepr::General(kind, desc, Some(detail.into())),
}
}
}
impl error::Error for RedisError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self.repr {
ErrorRepr::Internal { err, .. } => Some(err),
ErrorRepr::Server(err) => Some(err),
ErrorRepr::Parsing(err) => Some(err),
_ => None,
}
}
}
impl fmt::Debug for RedisError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt::Display::fmt(self, f)
}
}
impl fmt::Display for RedisError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match &self.repr {
ErrorRepr::General(kind, desc, detail) => {
desc.fmt(f)?;
f.write_str(" - ")?;
fmt::Debug::fmt(&kind, f)?;
if let Some(detail) = detail {
f.write_str(": ")?;
detail.fmt(f)
} else {
Ok(())
}
}
ErrorRepr::Internal { err, .. } => err.fmt(f),
ErrorRepr::Parsing(err) => err.fmt(f),
ErrorRepr::Server(err) => err.fmt(f),
ErrorRepr::Pipeline(items) => {
if items.len() > 1 {
f.write_str("Pipeline failures: [")?;
} else {
f.write_str("Pipeline failure: [")?;
}
let mut first = true;
for (index, error) in items.iter() {
if first {
write!(f, "(Index {index}, error: {error})")?;
first = false;
} else {
write!(f, ", (Index {index}, error: {error})")?;
}
}
f.write_str("]")
}
ErrorRepr::TransactionAborted(items) => {
f.write_str("Transaction aborted: [")?;
let mut first = true;
for (index, error) in items.iter() {
if first {
write!(f, "(Index {index}, error: {error})")?;
first = false;
} else {
write!(f, ", (Index {index}, error: {error})")?;
}
}
f.write_str("]")
}
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum RetryMethod {
Reconnect,
NoRetry,
RetryImmediately,
WaitAndRetry,
AskRedirect,
MovedRedirect,
ReconnectFromInitialConnections,
}
impl RedisError {
pub fn kind(&self) -> ErrorKind {
match &self.repr {
ErrorRepr::General(kind, _, _) => *kind,
ErrorRepr::Internal { kind, .. } => *kind,
ErrorRepr::Parsing(_) => ErrorKind::Parse,
ErrorRepr::Server(err) => match err.kind() {
Some(kind) => ErrorKind::Server(kind),
None => ErrorKind::Extension,
},
ErrorRepr::Pipeline(items) => items
.first()
.and_then(|item| item.1.kind().map(|kind| kind.into()))
.unwrap_or(ErrorKind::Extension),
ErrorRepr::TransactionAborted(..) => ErrorKind::Server(ServerErrorKind::ExecAbort),
}
}
pub fn detail(&self) -> Option<&str> {
match &self.repr {
ErrorRepr::General(_, _, detail) => detail.as_ref().map(|detail| detail.as_str()),
ErrorRepr::Parsing(err) => Some(&err.description),
ErrorRepr::Server(err) => err.details(),
_ => None,
}
}
pub fn code(&self) -> Option<&str> {
match self.kind() {
ErrorKind::Server(kind) => Some(kind.code()),
_ => match &self.repr {
ErrorRepr::Server(err) => Some(err.code()),
_ => None,
},
}
}
pub fn category(&self) -> &str {
match self.kind() {
ErrorKind::Server(ServerErrorKind::ResponseError) => "response error",
ErrorKind::AuthenticationFailed => "authentication failed",
ErrorKind::UnexpectedReturnType => "type error",
ErrorKind::Server(ServerErrorKind::ExecAbort) => "script execution aborted",
ErrorKind::Server(ServerErrorKind::BusyLoading) => "busy loading",
ErrorKind::Server(ServerErrorKind::NoScript) => "no script",
ErrorKind::InvalidClientConfig => "invalid client config",
ErrorKind::Server(ServerErrorKind::Moved) => "key moved",
ErrorKind::Server(ServerErrorKind::Ask) => "key moved (ask)",
ErrorKind::Server(ServerErrorKind::TryAgain) => "try again",
ErrorKind::Server(ServerErrorKind::ClusterDown) => "cluster down",
ErrorKind::Server(ServerErrorKind::CrossSlot) => "cross-slot",
ErrorKind::Server(ServerErrorKind::MasterDown) => "master down",
ErrorKind::Io => "I/O error",
ErrorKind::Extension => "extension error",
ErrorKind::Client => "client error",
ErrorKind::Server(ServerErrorKind::ReadOnly) => "read-only",
ErrorKind::MasterNameNotFoundBySentinel => "master name not found by sentinel",
ErrorKind::NoValidReplicasFoundBySentinel => "no valid replicas found by sentinel",
ErrorKind::EmptySentinelList => "empty sentinel list",
ErrorKind::Server(ServerErrorKind::NotBusy) => "not busy",
ErrorKind::ClusterConnectionNotFound => "connection to node in cluster not found",
#[cfg(feature = "json")]
ErrorKind::Serialize => "serializing",
ErrorKind::RESP3NotSupported => "resp3 is not supported by server",
ErrorKind::Parse => "parse error",
ErrorKind::Server(ServerErrorKind::NoSub) => {
"Server declined unsubscribe related command in non-subscribed mode"
}
ErrorKind::Server(ServerErrorKind::NoPerm) => "",
}
}
pub fn is_io_error(&self) -> bool {
self.kind() == ErrorKind::Io
}
pub(crate) fn as_io_error(&self) -> Option<&io::Error> {
match &self.repr {
ErrorRepr::Internal { err, .. } => err.downcast_ref(),
_ => None,
}
}
pub fn is_cluster_error(&self) -> bool {
matches!(
self.kind(),
ErrorKind::Server(ServerErrorKind::Moved)
| ErrorKind::Server(ServerErrorKind::Ask)
| ErrorKind::Server(ServerErrorKind::TryAgain)
| ErrorKind::Server(ServerErrorKind::ClusterDown)
)
}
pub fn is_connection_refusal(&self) -> bool {
self.as_io_error().is_some_and(|err| {
#[allow(clippy::match_like_matches_macro)]
match err.kind() {
io::ErrorKind::ConnectionRefused => true,
io::ErrorKind::NotFound => cfg!(unix),
_ => false,
}
})
}
pub fn is_timeout(&self) -> bool {
self.as_io_error().is_some_and(|err| {
matches!(
err.kind(),
io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock
)
})
}
pub fn is_connection_dropped(&self) -> bool {
match self.repr {
ErrorRepr::General(kind, _, _) => kind == ErrorKind::Io,
ErrorRepr::Internal { .. } => self.as_io_error().is_some_and(|err| {
matches!(
err.kind(),
io::ErrorKind::BrokenPipe
| io::ErrorKind::ConnectionReset
| io::ErrorKind::ConnectionRefused
| io::ErrorKind::ConnectionAborted
| io::ErrorKind::UnexpectedEof
| io::ErrorKind::NotConnected
| io::ErrorKind::NotFound
)
}),
_ => false,
}
}
pub fn is_unrecoverable_error(&self) -> bool {
let retry_method = self.retry_method();
match retry_method {
RetryMethod::Reconnect => true,
RetryMethod::ReconnectFromInitialConnections => true,
RetryMethod::NoRetry => false,
RetryMethod::RetryImmediately => false,
RetryMethod::WaitAndRetry => false,
RetryMethod::AskRedirect => false,
RetryMethod::MovedRedirect => false,
}
}
pub fn redirect_node(&self) -> Option<(&str, u16)> {
if !matches!(
self.kind(),
ErrorKind::Server(ServerErrorKind::Ask) | ErrorKind::Server(ServerErrorKind::Moved),
) {
return None;
}
let mut iter = self.detail()?.split_ascii_whitespace();
let slot_id: u16 = iter.next()?.parse().ok()?;
let addr = iter.next()?;
Some((addr, slot_id))
}
pub fn retry_method(&self) -> RetryMethod {
match self.kind() {
ErrorKind::Server(server_error) => server_error.retry_method(),
ErrorKind::MasterNameNotFoundBySentinel => RetryMethod::WaitAndRetry,
ErrorKind::NoValidReplicasFoundBySentinel => RetryMethod::WaitAndRetry,
ErrorKind::Extension => RetryMethod::NoRetry,
ErrorKind::UnexpectedReturnType => RetryMethod::NoRetry,
ErrorKind::InvalidClientConfig => RetryMethod::NoRetry,
ErrorKind::Client => RetryMethod::NoRetry,
ErrorKind::EmptySentinelList => RetryMethod::NoRetry,
#[cfg(feature = "json")]
ErrorKind::Serialize => RetryMethod::NoRetry,
ErrorKind::RESP3NotSupported => RetryMethod::NoRetry,
ErrorKind::Parse => RetryMethod::Reconnect,
ErrorKind::AuthenticationFailed => RetryMethod::Reconnect,
ErrorKind::ClusterConnectionNotFound => RetryMethod::ReconnectFromInitialConnections,
ErrorKind::Io => {
if self.is_connection_dropped() {
RetryMethod::Reconnect
} else {
self.as_io_error()
.map(|err| match err.kind() {
io::ErrorKind::PermissionDenied => RetryMethod::NoRetry,
io::ErrorKind::Unsupported => RetryMethod::NoRetry,
_ => RetryMethod::RetryImmediately,
})
.unwrap_or(RetryMethod::NoRetry)
}
}
}
}
pub fn into_server_errors(self) -> Option<Arc<[(usize, ServerError)]>> {
match self.repr {
ErrorRepr::Pipeline(items) => Some(items),
ErrorRepr::TransactionAborted(errs) => Some(errs),
ErrorRepr::Server(err) => Some(Arc::from([(0, err)])),
_ => None,
}
}
pub(crate) fn pipeline(errors: Vec<(usize, ServerError)>) -> Self {
Self {
repr: ErrorRepr::Pipeline(Arc::from(errors)),
}
}
pub(crate) fn make_aborted_transaction(errs: Vec<(usize, ServerError)>) -> Self {
Self {
repr: ErrorRepr::TransactionAborted(Arc::from(errs)),
}
}
pub(crate) fn make_empty_command() -> Self {
Self {
repr: ErrorRepr::General(ErrorKind::Client, "empty command", None),
}
}
}
pub fn make_extension_error(code: String, detail: Option<String>) -> RedisError {
RedisError {
repr: ErrorRepr::Server(ServerError(crate::errors::Repr::Extension {
code: code.into(),
detail: detail.map(|detail| detail.into()),
})),
}
}
#[cfg(feature = "tls-native-tls")]
impl From<native_tls::Error> for RedisError {
fn from(err: native_tls::Error) -> RedisError {
RedisError {
repr: ErrorRepr::Internal {
kind: ErrorKind::Client,
err: Arc::new(err),
},
}
}
}
#[cfg(feature = "tls-rustls")]
impl From<rustls::Error> for RedisError {
fn from(err: rustls::Error) -> RedisError {
RedisError {
repr: ErrorRepr::Internal {
kind: ErrorKind::Client,
err: Arc::new(err),
},
}
}
}
impl From<ServerError> for RedisError {
fn from(err: ServerError) -> Self {
Self {
repr: ErrorRepr::Server(err),
}
}
}
impl From<ServerErrorKind> for ErrorKind {
fn from(kind: ServerErrorKind) -> Self {
ErrorKind::Server(kind)
}
}
impl From<ParsingError> for RedisError {
fn from(err: ParsingError) -> Self {
RedisError {
repr: ErrorRepr::Parsing(err),
}
}
}
impl TryFrom<RedisError> for ServerError {
type Error = RedisError;
fn try_from(err: RedisError) -> Result<ServerError, RedisError> {
match err.repr {
ErrorRepr::Server(err) => Ok(err),
_ => Err(err),
}
}
}
#[cfg(test)]
mod tests {
use crate::parse_redis_value;
#[test]
fn test_redirect_node() {
let err = parse_redis_value(b"-ASK 123 foobar:6380\r\n")
.unwrap()
.extract_error()
.unwrap_err();
let node = err.redirect_node();
assert_eq!(node, Some(("foobar:6380", 123)));
}
}