Skip to main content

glide/
error.rs

1// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
2//! Error types for the GLIDE Rust client.
3//!
4//! Mirrors the Python exception hierarchy (`glide_shared.exceptions`):
5//! `GlideError` is the base; specific kinds map 1:1 to the Python subclasses.
6
7use redis::{ErrorKind, RedisError};
8use thiserror::Error;
9
10/// Base error type for the GLIDE client.
11#[derive(Debug, Error, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum GlideError {
14    /// The client is closed and can no longer be used. Unrecoverable.
15    #[error("ClosingError: {0}")]
16    Closing(String),
17
18    /// The client was misconfigured.
19    #[error("ConfigurationError: {0}")]
20    Configuration(String),
21
22    /// A connection could not be established or was lost.
23    #[error("ConnectionError: {0}")]
24    Connection(String),
25
26    /// A transaction (MULTI/EXEC) was aborted.
27    #[error("ExecAbortError: {0}")]
28    ExecAbort(String),
29
30    /// The server returned an error in response to a command.
31    #[error("RequestError: {0}")]
32    Request(String),
33
34    /// A request timed out.
35    #[error("TimeoutError: {0}")]
36    Timeout(String),
37
38    /// The client-side circuit breaker is open.
39    #[error("CircuitBreakerError: {0}")]
40    CircuitBreaker(String),
41}
42
43impl GlideError {
44    /// Short human-readable name of the error class (matches the Python class name).
45    pub fn class_name(&self) -> &'static str {
46        match self {
47            GlideError::Closing(_) => "ClosingError",
48            GlideError::Configuration(_) => "ConfigurationError",
49            GlideError::Connection(_) => "ConnectionError",
50            GlideError::ExecAbort(_) => "ExecAbortError",
51            GlideError::Request(_) => "RequestError",
52            GlideError::Timeout(_) => "TimeoutError",
53            GlideError::CircuitBreaker(_) => "CircuitBreakerError",
54        }
55    }
56
57    /// The message payload of the error.
58    pub fn message(&self) -> &str {
59        match self {
60            GlideError::Closing(m)
61            | GlideError::Configuration(m)
62            | GlideError::Connection(m)
63            | GlideError::ExecAbort(m)
64            | GlideError::Request(m)
65            | GlideError::Timeout(m)
66            | GlideError::CircuitBreaker(m) => m,
67        }
68    }
69
70    pub(crate) fn from_redis_error(err: RedisError) -> Self {
71        let msg = err.to_string();
72        // Mirror glide-core's `error_type` classifier (glide-core/src/errors.rs).
73        if err.is_timeout() {
74            GlideError::Timeout(msg)
75        } else if err.is_unrecoverable_error() {
76            GlideError::Connection(msg)
77        } else {
78            match err.kind() {
79                ErrorKind::ExecAbortError => GlideError::ExecAbort(msg),
80                ErrorKind::CircuitBreakerOpen => GlideError::CircuitBreaker(msg),
81                ErrorKind::InvalidClientConfig => GlideError::Configuration(msg),
82                _ => GlideError::Request(msg),
83            }
84        }
85    }
86
87    pub(crate) fn from_connection_error(err: glide_core::client::ConnectionError) -> Self {
88        use glide_core::client::ConnectionError as CE;
89        match err {
90            CE::Timeout => GlideError::Timeout("connection attempt timed out".to_string()),
91            CE::Configuration(msg) => GlideError::Configuration(msg),
92            other => GlideError::Connection(other.to_string()),
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    //! Pure-logic error tests: [`GlideError::from_redis_error`] and
100    //! [`GlideError::from_connection_error`] mapping into [`GlideError`], plus
101    //! `class_name()`/`message()` for every variant.
102    use super::*;
103    use glide_core::client::ConnectionError as CE;
104
105    // ---- From<RedisError> ------------------------------------------------
106
107    #[test]
108    fn redis_exec_abort_maps_to_exec_abort() {
109        let err = RedisError::from((ErrorKind::ExecAbortError, "aborted"));
110        let g = GlideError::from_redis_error(err);
111        assert!(matches!(g, GlideError::ExecAbort(_)));
112        assert_eq!(g.class_name(), "ExecAbortError");
113    }
114
115    #[test]
116    fn redis_circuit_breaker_maps_to_circuit_breaker() {
117        let err = RedisError::from((ErrorKind::CircuitBreakerOpen, "open"));
118        let g = GlideError::from_redis_error(err);
119        assert!(matches!(g, GlideError::CircuitBreaker(_)));
120        assert_eq!(g.class_name(), "CircuitBreakerError");
121    }
122
123    #[test]
124    fn redis_invalid_client_config_maps_to_configuration() {
125        let err = RedisError::from((ErrorKind::InvalidClientConfig, "bad config"));
126        let g = GlideError::from_redis_error(err);
127        assert!(matches!(g, GlideError::Configuration(_)));
128        assert_eq!(g.class_name(), "ConfigurationError");
129    }
130
131    #[test]
132    fn redis_response_error_maps_to_request() {
133        let err = RedisError::from((ErrorKind::ResponseError, "wrong type"));
134        let g = GlideError::from_redis_error(err);
135        assert!(matches!(g, GlideError::Request(_)));
136        assert_eq!(g.class_name(), "RequestError");
137    }
138
139    #[test]
140    fn redis_unknown_kind_maps_to_request() {
141        // A kind we do not special-case falls through to Request.
142        let err = RedisError::from((ErrorKind::TypeError, "type mismatch"));
143        let g = GlideError::from_redis_error(err);
144        assert!(matches!(g, GlideError::Request(_)));
145    }
146
147    #[test]
148    fn redis_io_error_maps_to_connection() {
149        let io_err = std::io::Error::from(std::io::ErrorKind::ConnectionReset);
150        let err: RedisError = io_err.into();
151        assert!(err.is_unrecoverable_error());
152        let g = GlideError::from_redis_error(err);
153        assert!(matches!(g, GlideError::Connection(_)));
154        assert_eq!(g.class_name(), "ConnectionError");
155    }
156
157    #[test]
158    fn redis_client_error_maps_to_request() {
159        let err = RedisError::from((ErrorKind::ClientError, "inflight limit reached"));
160        let g = GlideError::from_redis_error(err);
161        assert!(matches!(g, GlideError::Request(_)));
162        assert_eq!(g.class_name(), "RequestError");
163    }
164
165    #[test]
166    fn redis_real_io_timeout_maps_to_timeout() {
167        let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "slow");
168        let err: RedisError = io_err.into();
169        assert!(err.is_timeout());
170        assert!(matches!(
171            GlideError::from_redis_error(err),
172            GlideError::Timeout(_)
173        ));
174    }
175
176    #[test]
177    fn redis_error_preserves_message() {
178        let err = RedisError::from((ErrorKind::ResponseError, "custom detail"));
179        let g = GlideError::from_redis_error(err);
180        assert!(g.message().contains("custom detail"));
181    }
182
183    // ---- From<ConnectionError> ------------------------------------------
184
185    #[test]
186    fn connection_timeout_maps_to_timeout() {
187        let g = GlideError::from_connection_error(CE::Timeout);
188        assert!(matches!(g, GlideError::Timeout(_)));
189        assert_eq!(g.class_name(), "TimeoutError");
190    }
191
192    #[test]
193    fn connection_configuration_maps_to_configuration() {
194        let g = GlideError::from_connection_error(CE::Configuration("nope".to_string()));
195        assert!(matches!(g, GlideError::Configuration(_)));
196        assert_eq!(g.message(), "nope");
197    }
198
199    #[test]
200    fn connection_io_error_maps_to_connection() {
201        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
202        let g = GlideError::from_connection_error(CE::IoError(io_err));
203        assert!(matches!(g, GlideError::Connection(_)));
204        assert_eq!(g.class_name(), "ConnectionError");
205    }
206
207    #[test]
208    fn connection_cluster_error_maps_to_connection() {
209        let redis_err = RedisError::from((ErrorKind::ResponseError, "cluster boom"));
210        let g = GlideError::from_connection_error(CE::Cluster(redis_err));
211        assert!(matches!(g, GlideError::Connection(_)));
212    }
213
214    // ---- class_name() / message() for every variant --------------------
215
216    #[test]
217    fn class_names_for_all_variants() {
218        assert_eq!(GlideError::Closing("x".into()).class_name(), "ClosingError");
219        assert_eq!(
220            GlideError::Configuration("x".into()).class_name(),
221            "ConfigurationError"
222        );
223        assert_eq!(
224            GlideError::Connection("x".into()).class_name(),
225            "ConnectionError"
226        );
227        assert_eq!(
228            GlideError::ExecAbort("x".into()).class_name(),
229            "ExecAbortError"
230        );
231        assert_eq!(GlideError::Request("x".into()).class_name(), "RequestError");
232        assert_eq!(GlideError::Timeout("x".into()).class_name(), "TimeoutError");
233        assert_eq!(
234            GlideError::CircuitBreaker("x".into()).class_name(),
235            "CircuitBreakerError"
236        );
237    }
238
239    #[test]
240    fn message_for_all_variants() {
241        assert_eq!(GlideError::Closing("a".into()).message(), "a");
242        assert_eq!(GlideError::Configuration("b".into()).message(), "b");
243        assert_eq!(GlideError::Connection("c".into()).message(), "c");
244        assert_eq!(GlideError::ExecAbort("d".into()).message(), "d");
245        assert_eq!(GlideError::Request("e".into()).message(), "e");
246        assert_eq!(GlideError::Timeout("f".into()).message(), "f");
247        assert_eq!(GlideError::CircuitBreaker("g".into()).message(), "g");
248    }
249
250    #[test]
251    fn display_includes_class_and_message() {
252        let e = GlideError::Timeout("slow".into());
253        assert_eq!(e.to_string(), "TimeoutError: slow");
254    }
255
256    #[test]
257    fn variants_are_clonable_and_comparable() {
258        let a = GlideError::Request("same".into());
259        let b = a.clone();
260        assert_eq!(a, b);
261        assert_ne!(a, GlideError::Request("different".into()));
262    }
263}