redis/errors/
redis_error.rs

1use std::{error, fmt, io, sync::Arc};
2
3use arcstr::ArcStr;
4
5use crate::{
6    errors::server_error::{ServerError, ServerErrorKind},
7    ParsingError,
8};
9
10/// An enum of all error kinds.
11#[derive(PartialEq, Eq, Copy, Clone, Debug)]
12#[non_exhaustive]
13pub enum ErrorKind {
14    /// The parser failed to parse the server response.
15    Parse,
16    /// The authentication with the server failed.
17    AuthenticationFailed,
18    /// Operation failed because of a type mismatch.
19    UnexpectedReturnType,
20    /// An error that was caused because the parameter to the
21    /// client were wrong.
22    InvalidClientConfig,
23    /// This kind is returned if the redis error is one that is
24    /// not native to the system.  This is usually the case if
25    /// the cause is another error.
26    Io,
27    /// An error raised that was identified on the client before execution.
28    Client,
29    /// An extension error.  This is an error created by the server
30    /// that is not directly understood by the library.
31    Extension,
32    /// Requested name not found among masters returned by the sentinels
33    MasterNameNotFoundBySentinel,
34    /// No valid replicas found in the sentinels, for a given master name
35    NoValidReplicasFoundBySentinel,
36    /// At least one sentinel connection info is required
37    EmptySentinelList,
38    /// Used when a cluster connection cannot find a connection to a valid node.
39    ClusterConnectionNotFound,
40    /// An error returned from the server
41    Server(ServerErrorKind),
42
43    #[cfg(feature = "json")]
44    /// Error Serializing a struct to JSON form
45    Serialize,
46
47    /// Redis Servers prior to v6.0.0 doesn't support RESP3.
48    /// Try disabling resp3 option
49    RESP3NotSupported,
50}
51
52/// Represents a redis error.
53///
54/// For the most part you should be using the Error trait to interact with this
55/// rather than the actual struct.
56#[derive(Clone)]
57pub struct RedisError {
58    repr: ErrorRepr,
59}
60
61#[cfg(feature = "json")]
62impl From<serde_json::Error> for RedisError {
63    fn from(serde_err: serde_json::Error) -> RedisError {
64        RedisError {
65            repr: ErrorRepr::Internal {
66                kind: ErrorKind::Serialize,
67                err: Arc::new(serde_err),
68            },
69        }
70    }
71}
72
73#[derive(Debug, Clone)]
74enum ErrorRepr {
75    General(ErrorKind, &'static str, Option<ArcStr>),
76    Internal {
77        kind: ErrorKind,
78        err: Arc<dyn error::Error + Send + Sync>,
79    },
80    Parsing(ParsingError),
81    Server(ServerError),
82    Pipeline(Arc<[(usize, ServerError)]>),
83    TransactionAborted(Arc<[(usize, ServerError)]>),
84}
85
86impl PartialEq for RedisError {
87    fn eq(&self, other: &RedisError) -> bool {
88        match (&self.repr, &other.repr) {
89            (&ErrorRepr::General(kind_a, _, _), &ErrorRepr::General(kind_b, _, _)) => {
90                kind_a == kind_b
91            }
92            (ErrorRepr::Parsing(a), ErrorRepr::Parsing(b)) => *a == *b,
93            (ErrorRepr::Server(a), ErrorRepr::Server(b)) => *a == *b,
94            (ErrorRepr::Pipeline(a), ErrorRepr::Pipeline(b)) => *a == *b,
95            _ => false,
96        }
97    }
98}
99
100impl From<io::Error> for RedisError {
101    fn from(err: io::Error) -> RedisError {
102        RedisError {
103            repr: ErrorRepr::Internal {
104                kind: ErrorKind::Io,
105                err: Arc::new(err),
106            },
107        }
108    }
109}
110
111#[cfg(feature = "tls-rustls")]
112impl From<rustls::pki_types::InvalidDnsNameError> for RedisError {
113    fn from(err: rustls::pki_types::InvalidDnsNameError) -> RedisError {
114        RedisError {
115            repr: ErrorRepr::Internal {
116                kind: ErrorKind::Io,
117                err: Arc::new(err),
118            },
119        }
120    }
121}
122
123#[cfg(feature = "tls-rustls")]
124impl From<rustls_native_certs::Error> for RedisError {
125    fn from(err: rustls_native_certs::Error) -> RedisError {
126        RedisError {
127            repr: ErrorRepr::Internal {
128                kind: ErrorKind::Io,
129                err: Arc::new(err),
130            },
131        }
132    }
133}
134
135impl From<(ErrorKind, &'static str)> for RedisError {
136    fn from((kind, desc): (ErrorKind, &'static str)) -> RedisError {
137        RedisError {
138            repr: ErrorRepr::General(kind, desc, None),
139        }
140    }
141}
142
143impl From<(ErrorKind, &'static str, String)> for RedisError {
144    fn from((kind, desc, detail): (ErrorKind, &'static str, String)) -> RedisError {
145        RedisError {
146            repr: ErrorRepr::General(kind, desc, Some(detail.into())),
147        }
148    }
149}
150
151impl error::Error for RedisError {
152    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
153        match &self.repr {
154            ErrorRepr::Internal { err, .. } => Some(err),
155            ErrorRepr::Server(err) => Some(err),
156            ErrorRepr::Parsing(err) => Some(err),
157            _ => None,
158        }
159    }
160}
161
162impl fmt::Debug for RedisError {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
164        fmt::Display::fmt(self, f)
165    }
166}
167
168impl fmt::Display for RedisError {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
170        match &self.repr {
171            ErrorRepr::General(kind, desc, detail) => {
172                desc.fmt(f)?;
173                f.write_str(" - ")?;
174                fmt::Debug::fmt(&kind, f)?;
175                if let Some(detail) = detail {
176                    f.write_str(": ")?;
177                    detail.fmt(f)
178                } else {
179                    Ok(())
180                }
181            }
182            ErrorRepr::Internal { err, .. } => err.fmt(f),
183            ErrorRepr::Parsing(err) => err.fmt(f),
184            ErrorRepr::Server(err) => err.fmt(f),
185            ErrorRepr::Pipeline(items) => {
186                if items.len() > 1 {
187                    f.write_str("Pipeline failures: [")?;
188                } else {
189                    f.write_str("Pipeline failure: [")?;
190                }
191                let mut first = true;
192                for (index, error) in items.iter() {
193                    if first {
194                        write!(f, "(Index {index}, error: {error})")?;
195                        first = false;
196                    } else {
197                        write!(f, ", (Index {index}, error: {error})")?;
198                    }
199                }
200                f.write_str("]")
201            }
202            ErrorRepr::TransactionAborted(items) => {
203                f.write_str("Transaction aborted: [")?;
204
205                let mut first = true;
206                for (index, error) in items.iter() {
207                    if first {
208                        write!(f, "(Index {index}, error: {error})")?;
209                        first = false;
210                    } else {
211                        write!(f, ", (Index {index}, error: {error})")?;
212                    }
213                }
214                f.write_str("]")
215            }
216        }
217    }
218}
219
220/// What method should be used if retrying this request.
221#[non_exhaustive]
222pub enum RetryMethod {
223    /// Create a fresh connection, since the current connection is no longer usable.
224    Reconnect,
225    /// Don't retry, this is a permanent error.
226    NoRetry,
227    /// Retry immediately, this doesn't require a wait.
228    RetryImmediately,
229    /// Retry after sleeping to avoid overloading the external service.
230    WaitAndRetry,
231    /// The key has moved to a different node but we have to ask which node, this is only relevant for clusters.
232    AskRedirect,
233    /// The key has moved to a different node, this is only relevant for clusters.
234    MovedRedirect,
235    /// Reconnect the initial connection to the master cluster, this is only relevant for clusters.
236    ReconnectFromInitialConnections,
237}
238
239/// Indicates a general failure in the library.
240impl RedisError {
241    /// Returns the kind of the error.
242    pub fn kind(&self) -> ErrorKind {
243        match &self.repr {
244            ErrorRepr::General(kind, _, _) => *kind,
245            ErrorRepr::Internal { kind, .. } => *kind,
246            ErrorRepr::Parsing(_) => ErrorKind::Parse,
247            ErrorRepr::Server(err) => match err.kind() {
248                Some(kind) => ErrorKind::Server(kind),
249                None => ErrorKind::Extension,
250            },
251            ErrorRepr::Pipeline(items) => items
252                .first()
253                .and_then(|item| item.1.kind().map(|kind| kind.into()))
254                .unwrap_or(ErrorKind::Extension),
255            ErrorRepr::TransactionAborted(..) => ErrorKind::Server(ServerErrorKind::ExecAbort),
256        }
257    }
258
259    /// Returns the error detail.
260    pub fn detail(&self) -> Option<&str> {
261        match &self.repr {
262            ErrorRepr::General(_, _, detail) => detail.as_ref().map(|detail| detail.as_str()),
263            ErrorRepr::Parsing(err) => Some(&err.description),
264            ErrorRepr::Server(err) => err.details(),
265            _ => None,
266        }
267    }
268
269    /// Returns the raw error code if available.
270    pub fn code(&self) -> Option<&str> {
271        match self.kind() {
272            ErrorKind::Server(kind) => Some(kind.code()),
273            _ => match &self.repr {
274                ErrorRepr::Server(err) => Some(err.code()),
275                _ => None,
276            },
277        }
278    }
279
280    /// Returns the name of the error category for display purposes.
281    pub fn category(&self) -> &str {
282        match self.kind() {
283            ErrorKind::Server(ServerErrorKind::ResponseError) => "response error",
284            ErrorKind::AuthenticationFailed => "authentication failed",
285            ErrorKind::UnexpectedReturnType => "type error",
286            ErrorKind::Server(ServerErrorKind::ExecAbort) => "script execution aborted",
287            ErrorKind::Server(ServerErrorKind::BusyLoading) => "busy loading",
288            ErrorKind::Server(ServerErrorKind::NoScript) => "no script",
289            ErrorKind::InvalidClientConfig => "invalid client config",
290            ErrorKind::Server(ServerErrorKind::Moved) => "key moved",
291            ErrorKind::Server(ServerErrorKind::Ask) => "key moved (ask)",
292            ErrorKind::Server(ServerErrorKind::TryAgain) => "try again",
293            ErrorKind::Server(ServerErrorKind::ClusterDown) => "cluster down",
294            ErrorKind::Server(ServerErrorKind::CrossSlot) => "cross-slot",
295            ErrorKind::Server(ServerErrorKind::MasterDown) => "master down",
296            ErrorKind::Io => "I/O error",
297            ErrorKind::Extension => "extension error",
298            ErrorKind::Client => "client error",
299            ErrorKind::Server(ServerErrorKind::ReadOnly) => "read-only",
300            ErrorKind::MasterNameNotFoundBySentinel => "master name not found by sentinel",
301            ErrorKind::NoValidReplicasFoundBySentinel => "no valid replicas found by sentinel",
302            ErrorKind::EmptySentinelList => "empty sentinel list",
303            ErrorKind::Server(ServerErrorKind::NotBusy) => "not busy",
304            ErrorKind::ClusterConnectionNotFound => "connection to node in cluster not found",
305            #[cfg(feature = "json")]
306            ErrorKind::Serialize => "serializing",
307            ErrorKind::RESP3NotSupported => "resp3 is not supported by server",
308            ErrorKind::Parse => "parse error",
309            ErrorKind::Server(ServerErrorKind::NoSub) => {
310                "Server declined unsubscribe related command in non-subscribed mode"
311            }
312            ErrorKind::Server(ServerErrorKind::NoPerm) => "",
313        }
314    }
315
316    /// Indicates that this failure is an IO failure.
317    pub fn is_io_error(&self) -> bool {
318        self.kind() == ErrorKind::Io
319    }
320
321    pub(crate) fn as_io_error(&self) -> Option<&io::Error> {
322        match &self.repr {
323            ErrorRepr::Internal { err, .. } => err.downcast_ref(),
324            _ => None,
325        }
326    }
327
328    /// Indicates that this is a cluster error.
329    pub fn is_cluster_error(&self) -> bool {
330        matches!(
331            self.kind(),
332            ErrorKind::Server(ServerErrorKind::Moved)
333                | ErrorKind::Server(ServerErrorKind::Ask)
334                | ErrorKind::Server(ServerErrorKind::TryAgain)
335                | ErrorKind::Server(ServerErrorKind::ClusterDown)
336        )
337    }
338
339    /// Returns true if this error indicates that the connection was
340    /// refused.  You should generally not rely much on this function
341    /// unless you are writing unit tests that want to detect if a
342    /// local server is available.
343    pub fn is_connection_refusal(&self) -> bool {
344        self.as_io_error().is_some_and(|err| {
345            #[allow(clippy::match_like_matches_macro)]
346            match err.kind() {
347                io::ErrorKind::ConnectionRefused => true,
348                // if we connect to a unix socket and the file does not
349                // exist yet, then we want to treat this as if it was a
350                // connection refusal.
351                io::ErrorKind::NotFound => cfg!(unix),
352                _ => false,
353            }
354        })
355    }
356
357    /// Returns true if error was caused by I/O time out.
358    /// Note that this may not be accurate depending on platform.
359    pub fn is_timeout(&self) -> bool {
360        self.as_io_error().is_some_and(|err| {
361            matches!(
362                err.kind(),
363                io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock
364            )
365        })
366    }
367
368    /// Returns true if error was caused by a dropped connection.
369    pub fn is_connection_dropped(&self) -> bool {
370        self.as_io_error().is_some_and(|err| {
371            matches!(
372                err.kind(),
373                io::ErrorKind::BrokenPipe
374                    | io::ErrorKind::ConnectionReset
375                    | io::ErrorKind::UnexpectedEof
376            )
377        })
378    }
379
380    /// Returns true if the error is likely to not be recoverable, and the connection must be replaced.
381    pub fn is_unrecoverable_error(&self) -> bool {
382        match self.retry_method() {
383            RetryMethod::Reconnect => true,
384            RetryMethod::ReconnectFromInitialConnections => true,
385
386            RetryMethod::NoRetry => false,
387            RetryMethod::RetryImmediately => false,
388            RetryMethod::WaitAndRetry => false,
389            RetryMethod::AskRedirect => false,
390            RetryMethod::MovedRedirect => false,
391        }
392    }
393
394    /// Returns the node the error refers to.
395    ///
396    /// This returns `(addr, slot_id)`.
397    pub fn redirect_node(&self) -> Option<(&str, u16)> {
398        if !matches!(
399            self.kind(),
400            ErrorKind::Server(ServerErrorKind::Ask) | ErrorKind::Server(ServerErrorKind::Moved),
401        ) {
402            return None;
403        }
404        let mut iter = self.detail()?.split_ascii_whitespace();
405        let slot_id: u16 = iter.next()?.parse().ok()?;
406        let addr = iter.next()?;
407        Some((addr, slot_id))
408    }
409
410    /// Specifies what method (if any) should be used to retry this request.
411    ///
412    /// If you are using the cluster api retrying of requests is already handled by the library.
413    ///
414    /// This isn't precise, and internally the library uses multiple other considerations rather
415    /// than just the error kind on when to retry.
416    pub fn retry_method(&self) -> RetryMethod {
417        match self.kind() {
418            ErrorKind::Server(server_error) => server_error.retry_method(),
419
420            ErrorKind::MasterNameNotFoundBySentinel => RetryMethod::WaitAndRetry,
421            ErrorKind::NoValidReplicasFoundBySentinel => RetryMethod::WaitAndRetry,
422
423            ErrorKind::Extension => RetryMethod::NoRetry,
424            ErrorKind::UnexpectedReturnType => RetryMethod::NoRetry,
425            ErrorKind::InvalidClientConfig => RetryMethod::NoRetry,
426            ErrorKind::Client => RetryMethod::NoRetry,
427            ErrorKind::EmptySentinelList => RetryMethod::NoRetry,
428            #[cfg(feature = "json")]
429            ErrorKind::Serialize => RetryMethod::NoRetry,
430            ErrorKind::RESP3NotSupported => RetryMethod::NoRetry,
431
432            ErrorKind::Parse => RetryMethod::Reconnect,
433            ErrorKind::AuthenticationFailed => RetryMethod::Reconnect,
434            ErrorKind::ClusterConnectionNotFound => RetryMethod::ReconnectFromInitialConnections,
435
436            ErrorKind::Io => self
437                .as_io_error()
438                .map(|err| match err.kind() {
439                    io::ErrorKind::ConnectionRefused => RetryMethod::Reconnect,
440                    io::ErrorKind::NotFound => RetryMethod::Reconnect,
441                    io::ErrorKind::ConnectionReset => RetryMethod::Reconnect,
442                    io::ErrorKind::ConnectionAborted => RetryMethod::Reconnect,
443                    io::ErrorKind::NotConnected => RetryMethod::Reconnect,
444                    io::ErrorKind::BrokenPipe => RetryMethod::Reconnect,
445                    io::ErrorKind::UnexpectedEof => RetryMethod::Reconnect,
446
447                    io::ErrorKind::PermissionDenied => RetryMethod::NoRetry,
448                    io::ErrorKind::Unsupported => RetryMethod::NoRetry,
449
450                    _ => RetryMethod::RetryImmediately,
451                })
452                .unwrap_or(RetryMethod::NoRetry),
453        }
454    }
455
456    /// Returns the internal server errors, if there are any, and the failing commands indices.
457    ///
458    /// If this is called over over a pipeline or transaction error, the indices correspond to the positions of the failing commands in the pipeline or transaction.
459    /// If the error is not a pipeline error, the index will be 0.
460    pub fn into_server_errors(self) -> Option<Arc<[(usize, ServerError)]>> {
461        match self.repr {
462            ErrorRepr::Pipeline(items) => Some(items),
463            ErrorRepr::TransactionAborted(errs) => Some(errs),
464            ErrorRepr::Server(err) => Some(Arc::from([(0, err)])),
465            _ => None,
466        }
467    }
468
469    pub(crate) fn pipeline(errors: Vec<(usize, ServerError)>) -> Self {
470        Self {
471            repr: ErrorRepr::Pipeline(Arc::from(errors)),
472        }
473    }
474
475    pub(crate) fn make_aborted_transaction(errs: Vec<(usize, ServerError)>) -> Self {
476        Self {
477            repr: ErrorRepr::TransactionAborted(Arc::from(errs)),
478        }
479    }
480
481    pub(crate) fn make_empty_command() -> Self {
482        Self {
483            repr: ErrorRepr::General(ErrorKind::Client, "empty command", None),
484        }
485    }
486}
487
488/// Creates a new Redis error with the `Extension` kind.
489///
490/// This function is used to create Redis errors for extension error codes
491/// that are not directly understood by the library.
492///
493/// # Arguments
494///
495/// * `code` - The error code string returned by the Redis server
496/// * `detail` - Optional detailed error message. If None, a default message is used.
497///
498/// # Returns
499///
500/// A `RedisError` with the `Extension` kind.
501pub fn make_extension_error(code: String, detail: Option<String>) -> RedisError {
502    RedisError {
503        repr: ErrorRepr::Server(ServerError(crate::errors::Repr::Extension {
504            code: code.into(),
505            detail: detail.map(|detail| detail.into()),
506        })),
507    }
508}
509
510#[cfg(feature = "tls-native-tls")]
511impl From<native_tls::Error> for RedisError {
512    fn from(err: native_tls::Error) -> RedisError {
513        RedisError {
514            repr: ErrorRepr::Internal {
515                kind: ErrorKind::Client,
516                err: Arc::new(err),
517            },
518        }
519    }
520}
521
522#[cfg(feature = "tls-rustls")]
523impl From<rustls::Error> for RedisError {
524    fn from(err: rustls::Error) -> RedisError {
525        RedisError {
526            repr: ErrorRepr::Internal {
527                kind: ErrorKind::Client,
528                err: Arc::new(err),
529            },
530        }
531    }
532}
533
534impl From<ServerError> for RedisError {
535    fn from(err: ServerError) -> Self {
536        Self {
537            repr: ErrorRepr::Server(err),
538        }
539    }
540}
541
542impl From<ServerErrorKind> for ErrorKind {
543    fn from(kind: ServerErrorKind) -> Self {
544        ErrorKind::Server(kind)
545    }
546}
547
548impl From<ParsingError> for RedisError {
549    fn from(err: ParsingError) -> Self {
550        RedisError {
551            repr: ErrorRepr::Parsing(err),
552        }
553    }
554}
555
556impl TryFrom<RedisError> for ServerError {
557    type Error = RedisError;
558
559    fn try_from(err: RedisError) -> Result<ServerError, RedisError> {
560        match err.repr {
561            ErrorRepr::Server(err) => Ok(err),
562            _ => Err(err),
563        }
564    }
565}
566
567#[cfg(test)]
568mod tests {
569    use crate::parse_redis_value;
570
571    #[test]
572    fn test_redirect_node() {
573        let err = parse_redis_value(b"-ASK 123 foobar:6380\r\n")
574            .unwrap()
575            .extract_error()
576            .unwrap_err();
577        let node = err.redirect_node();
578
579        assert_eq!(node, Some(("foobar:6380", 123)));
580    }
581}