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