Skip to main content

redis/errors/
redis_error.rs

1use std::{error, fmt, io, sync::Arc};
2
3use arcstr::ArcStr;
4
5use crate::{
6    ParsingError,
7    errors::server_error::{ServerError, ServerErrorKind},
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#[derive(Debug, Clone)]
222#[non_exhaustive]
223pub enum RetryMethod {
224    /// Create a fresh connection, since the current connection is no longer usable.
225    Reconnect,
226    /// Don't retry, this is a permanent error.
227    NoRetry,
228    /// Retry immediately, this doesn't require a wait.
229    RetryImmediately,
230    /// Retry after sleeping to avoid overloading the external service.
231    WaitAndRetry,
232    /// The key has moved to a different node but we have to ask which node, this is only relevant for clusters.
233    AskRedirect,
234    /// The key has moved to a different node, this is only relevant for clusters.
235    MovedRedirect,
236    /// Reconnect the initial connection to the master cluster, this is only relevant for clusters.
237    ReconnectFromInitialConnections,
238}
239
240/// Indicates a general failure in the library.
241impl RedisError {
242    /// Returns the kind of the error.
243    pub fn kind(&self) -> ErrorKind {
244        match &self.repr {
245            ErrorRepr::General(kind, _, _) => *kind,
246            ErrorRepr::Internal { kind, .. } => *kind,
247            ErrorRepr::Parsing(_) => ErrorKind::Parse,
248            ErrorRepr::Server(err) => match err.kind() {
249                Some(kind) => ErrorKind::Server(kind),
250                None => ErrorKind::Extension,
251            },
252            ErrorRepr::Pipeline(items) => items
253                .first()
254                .and_then(|item| item.1.kind().map(|kind| kind.into()))
255                .unwrap_or(ErrorKind::Extension),
256            ErrorRepr::TransactionAborted(..) => ErrorKind::Server(ServerErrorKind::ExecAbort),
257        }
258    }
259
260    /// Returns the error detail.
261    pub fn detail(&self) -> Option<&str> {
262        match &self.repr {
263            ErrorRepr::General(_, _, detail) => detail.as_ref().map(|detail| detail.as_str()),
264            ErrorRepr::Parsing(err) => Some(&err.description),
265            ErrorRepr::Server(err) => err.details(),
266            _ => None,
267        }
268    }
269
270    /// Returns the raw error code if available.
271    pub fn code(&self) -> Option<&str> {
272        match self.kind() {
273            ErrorKind::Server(kind) => Some(kind.code()),
274            _ => match &self.repr {
275                ErrorRepr::Server(err) => Some(err.code()),
276                _ => None,
277            },
278        }
279    }
280
281    /// Returns the name of the error category for display purposes.
282    pub fn category(&self) -> &str {
283        match self.kind() {
284            ErrorKind::Server(ServerErrorKind::ResponseError) => "response error",
285            ErrorKind::AuthenticationFailed => "authentication failed",
286            ErrorKind::UnexpectedReturnType => "type error",
287            ErrorKind::Server(ServerErrorKind::ExecAbort) => "script execution aborted",
288            ErrorKind::Server(ServerErrorKind::BusyLoading) => "busy loading",
289            ErrorKind::Server(ServerErrorKind::NoScript) => "no script",
290            ErrorKind::InvalidClientConfig => "invalid client config",
291            ErrorKind::Server(ServerErrorKind::Moved) => "key moved",
292            ErrorKind::Server(ServerErrorKind::Ask) => "key moved (ask)",
293            ErrorKind::Server(ServerErrorKind::TryAgain) => "try again",
294            ErrorKind::Server(ServerErrorKind::ClusterDown) => "cluster down",
295            ErrorKind::Server(ServerErrorKind::CrossSlot) => "cross-slot",
296            ErrorKind::Server(ServerErrorKind::MasterDown) => "master down",
297            ErrorKind::Io => "I/O error",
298            ErrorKind::Extension => "extension error",
299            ErrorKind::Client => "client error",
300            ErrorKind::Server(ServerErrorKind::ReadOnly) => "read-only",
301            ErrorKind::MasterNameNotFoundBySentinel => "master name not found by sentinel",
302            ErrorKind::NoValidReplicasFoundBySentinel => "no valid replicas found by sentinel",
303            ErrorKind::EmptySentinelList => "empty sentinel list",
304            ErrorKind::Server(ServerErrorKind::NotBusy) => "not busy",
305            ErrorKind::ClusterConnectionNotFound => "connection to node in cluster not found",
306            #[cfg(feature = "json")]
307            ErrorKind::Serialize => "serializing",
308            ErrorKind::RESP3NotSupported => "resp3 is not supported by server",
309            ErrorKind::Parse => "parse error",
310            ErrorKind::Server(ServerErrorKind::NoSub) => {
311                "Server declined unsubscribe related command in non-subscribed mode"
312            }
313            ErrorKind::Server(ServerErrorKind::NoPerm) => "",
314        }
315    }
316
317    /// Indicates that this failure is an IO failure.
318    pub fn is_io_error(&self) -> bool {
319        self.kind() == ErrorKind::Io
320    }
321
322    pub(crate) fn as_io_error(&self) -> Option<&io::Error> {
323        match &self.repr {
324            ErrorRepr::Internal { err, .. } => err.downcast_ref(),
325            _ => None,
326        }
327    }
328
329    /// Indicates that this is a cluster error.
330    pub fn is_cluster_error(&self) -> bool {
331        matches!(
332            self.kind(),
333            ErrorKind::Server(ServerErrorKind::Moved)
334                | ErrorKind::Server(ServerErrorKind::Ask)
335                | ErrorKind::Server(ServerErrorKind::TryAgain)
336                | ErrorKind::Server(ServerErrorKind::ClusterDown)
337        )
338    }
339
340    /// Returns true if this error indicates that the connection was
341    /// refused.  You should generally not rely much on this function
342    /// unless you are writing unit tests that want to detect if a
343    /// local server is available.
344    pub fn is_connection_refusal(&self) -> bool {
345        self.as_io_error().is_some_and(|err| {
346            #[allow(clippy::match_like_matches_macro)]
347            match err.kind() {
348                io::ErrorKind::ConnectionRefused => true,
349                // if we connect to a unix socket and the file does not
350                // exist yet, then we want to treat this as if it was a
351                // connection refusal.
352                io::ErrorKind::NotFound => cfg!(unix),
353                _ => false,
354            }
355        })
356    }
357
358    /// Returns true if error was caused by I/O time out.
359    /// Note that this may not be accurate depending on platform.
360    pub fn is_timeout(&self) -> bool {
361        self.as_io_error().is_some_and(|err| {
362            matches!(
363                err.kind(),
364                io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock
365            )
366        })
367    }
368
369    /// Returns true if error was caused by a dropped connection.
370    pub fn is_connection_dropped(&self) -> bool {
371        match self.repr {
372            ErrorRepr::General(kind, _, _) => kind == ErrorKind::Io,
373            ErrorRepr::Internal { .. } => self.as_io_error().is_some_and(|err| {
374                matches!(
375                    err.kind(),
376                    io::ErrorKind::BrokenPipe
377                        | io::ErrorKind::ConnectionReset
378                        | io::ErrorKind::ConnectionRefused
379                        | io::ErrorKind::ConnectionAborted
380                        | io::ErrorKind::UnexpectedEof
381                        | io::ErrorKind::NotConnected
382                        | io::ErrorKind::NotFound
383                )
384            }),
385
386            _ => false,
387        }
388    }
389
390    /// Returns true if the error is likely to not be recoverable, and the connection must be replaced.
391    pub fn is_unrecoverable_error(&self) -> bool {
392        let retry_method = self.retry_method();
393        match retry_method {
394            RetryMethod::Reconnect => true,
395            RetryMethod::ReconnectFromInitialConnections => true,
396
397            RetryMethod::NoRetry => false,
398            RetryMethod::RetryImmediately => false,
399            RetryMethod::WaitAndRetry => false,
400            RetryMethod::AskRedirect => false,
401            RetryMethod::MovedRedirect => false,
402        }
403    }
404
405    /// Returns the node the error refers to.
406    ///
407    /// This returns `(addr, slot_id)`.
408    pub fn redirect_node(&self) -> Option<(&str, u16)> {
409        if !matches!(
410            self.kind(),
411            ErrorKind::Server(ServerErrorKind::Ask) | ErrorKind::Server(ServerErrorKind::Moved),
412        ) {
413            return None;
414        }
415        let mut iter = self.detail()?.split_ascii_whitespace();
416        let slot_id: u16 = iter.next()?.parse().ok()?;
417        let addr = iter.next()?;
418        Some((addr, slot_id))
419    }
420
421    /// Specifies what method (if any) should be used to retry this request.
422    ///
423    /// If you are using the cluster api retrying of requests is already handled by the library.
424    ///
425    /// This isn't precise, and internally the library uses multiple other considerations rather
426    /// than just the error kind on when to retry.
427    pub fn retry_method(&self) -> RetryMethod {
428        match self.kind() {
429            ErrorKind::Server(server_error) => server_error.retry_method(),
430
431            ErrorKind::MasterNameNotFoundBySentinel => RetryMethod::WaitAndRetry,
432            ErrorKind::NoValidReplicasFoundBySentinel => RetryMethod::WaitAndRetry,
433
434            ErrorKind::Extension => RetryMethod::NoRetry,
435            ErrorKind::UnexpectedReturnType => RetryMethod::NoRetry,
436            ErrorKind::InvalidClientConfig => RetryMethod::NoRetry,
437            ErrorKind::Client => RetryMethod::NoRetry,
438            ErrorKind::EmptySentinelList => RetryMethod::NoRetry,
439            #[cfg(feature = "json")]
440            ErrorKind::Serialize => RetryMethod::NoRetry,
441            ErrorKind::RESP3NotSupported => RetryMethod::NoRetry,
442
443            ErrorKind::Parse => RetryMethod::Reconnect,
444            ErrorKind::AuthenticationFailed => RetryMethod::Reconnect,
445            ErrorKind::ClusterConnectionNotFound => RetryMethod::ReconnectFromInitialConnections,
446
447            ErrorKind::Io => {
448                if self.is_connection_dropped() {
449                    RetryMethod::Reconnect
450                } else {
451                    self.as_io_error()
452                        .map(|err| match err.kind() {
453                            io::ErrorKind::PermissionDenied => RetryMethod::NoRetry,
454                            io::ErrorKind::Unsupported => RetryMethod::NoRetry,
455
456                            _ => RetryMethod::RetryImmediately,
457                        })
458                        .unwrap_or(RetryMethod::NoRetry)
459                }
460            }
461        }
462    }
463
464    /// Returns the internal server errors, if there are any, and the failing commands indices.
465    ///
466    /// 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.
467    /// If the error is not a pipeline error, the index will be 0.
468    pub fn into_server_errors(self) -> Option<Arc<[(usize, ServerError)]>> {
469        match self.repr {
470            ErrorRepr::Pipeline(items) => Some(items),
471            ErrorRepr::TransactionAborted(errs) => Some(errs),
472            ErrorRepr::Server(err) => Some(Arc::from([(0, err)])),
473            _ => None,
474        }
475    }
476
477    pub(crate) fn pipeline(errors: Vec<(usize, ServerError)>) -> Self {
478        Self {
479            repr: ErrorRepr::Pipeline(Arc::from(errors)),
480        }
481    }
482
483    pub(crate) fn make_aborted_transaction(errs: Vec<(usize, ServerError)>) -> Self {
484        Self {
485            repr: ErrorRepr::TransactionAborted(Arc::from(errs)),
486        }
487    }
488
489    pub(crate) fn make_empty_command() -> Self {
490        Self {
491            repr: ErrorRepr::General(ErrorKind::Client, "empty command", None),
492        }
493    }
494}
495
496/// Creates a new Redis error with the `Extension` kind.
497///
498/// This function is used to create Redis errors for extension error codes
499/// that are not directly understood by the library.
500///
501/// # Arguments
502///
503/// * `code` - The error code string returned by the Redis server
504/// * `detail` - Optional detailed error message. If None, a default message is used.
505///
506/// # Returns
507///
508/// A `RedisError` with the `Extension` kind.
509pub fn make_extension_error(code: String, detail: Option<String>) -> RedisError {
510    RedisError {
511        repr: ErrorRepr::Server(ServerError(crate::errors::Repr::Extension {
512            code: code.into(),
513            detail: detail.map(|detail| detail.into()),
514        })),
515    }
516}
517
518#[cfg(feature = "tls-native-tls")]
519impl From<native_tls::Error> for RedisError {
520    fn from(err: native_tls::Error) -> RedisError {
521        RedisError {
522            repr: ErrorRepr::Internal {
523                kind: ErrorKind::Client,
524                err: Arc::new(err),
525            },
526        }
527    }
528}
529
530#[cfg(feature = "tls-rustls")]
531impl From<rustls::Error> for RedisError {
532    fn from(err: rustls::Error) -> RedisError {
533        RedisError {
534            repr: ErrorRepr::Internal {
535                kind: ErrorKind::Client,
536                err: Arc::new(err),
537            },
538        }
539    }
540}
541
542impl From<ServerError> for RedisError {
543    fn from(err: ServerError) -> Self {
544        Self {
545            repr: ErrorRepr::Server(err),
546        }
547    }
548}
549
550impl From<ServerErrorKind> for ErrorKind {
551    fn from(kind: ServerErrorKind) -> Self {
552        ErrorKind::Server(kind)
553    }
554}
555
556impl From<ParsingError> for RedisError {
557    fn from(err: ParsingError) -> Self {
558        RedisError {
559            repr: ErrorRepr::Parsing(err),
560        }
561    }
562}
563
564impl TryFrom<RedisError> for ServerError {
565    type Error = RedisError;
566
567    fn try_from(err: RedisError) -> Result<ServerError, RedisError> {
568        match err.repr {
569            ErrorRepr::Server(err) => Ok(err),
570            _ => Err(err),
571        }
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use crate::parse_redis_value;
578
579    #[test]
580    fn test_redirect_node() {
581        let err = parse_redis_value(b"-ASK 123 foobar:6380\r\n")
582            .unwrap()
583            .extract_error()
584            .unwrap_err();
585        let node = err.redirect_node();
586
587        assert_eq!(node, Some(("foobar:6380", 123)));
588    }
589}