Skip to main content

ark_rest/
error.rs

1use crate::conversions::ConversionError;
2use std::error::Error as StdError;
3use std::fmt;
4
5type Source = Box<dyn StdError + Send + Sync + 'static>;
6
7pub struct Error {
8    inner: ErrorImpl,
9}
10
11struct ErrorImpl {
12    kind: Kind,
13    source: Option<Source>,
14}
15
16#[derive(Debug)]
17enum Kind {
18    Request,
19    Conversion,
20    ServerInfoChanged,
21}
22
23impl Error {
24    fn new(kind: Kind) -> Self {
25        Self {
26            inner: ErrorImpl { kind, source: None },
27        }
28    }
29
30    pub(crate) fn with(mut self, source: impl Into<Source>) -> Self {
31        self.inner.source = Some(source.into());
32        self
33    }
34
35    pub(crate) fn request(source: impl Into<Source>) -> Self {
36        Error::new(Kind::Request).with(source)
37    }
38
39    pub(crate) fn conversion(source: impl Into<Source>) -> Self {
40        Error::new(Kind::Conversion).with(source)
41    }
42
43    pub(crate) fn server_info_changed(source: impl Into<Source>) -> Self {
44        Error::new(Kind::ServerInfoChanged).with(source)
45    }
46
47    /// Returns `true` if the failed operation triggered a digest-mismatch refresh of the
48    /// cached `/info`. The original request was not retried.
49    pub fn is_server_info_changed(&self) -> bool {
50        matches!(self.inner.kind, Kind::ServerInfoChanged)
51    }
52
53    /// Returns `true` if the server rejected the request because the SDK
54    /// version is too old.
55    pub fn is_version_mismatch(&self) -> bool {
56        self.source_contains_any(&["BUILD_VERSION_TOO_OLD"])
57    }
58
59    /// Returns `true` if the server rejected the request because the cached
60    /// `/info` digest is stale.
61    pub(crate) fn is_digest_mismatch(&self) -> bool {
62        matches!(self.inner.kind, Kind::Request)
63            && self.source_contains_any(&["DIGEST_MISMATCH", "invalid digest header"])
64    }
65
66    fn source_contains_any(&self, markers: &[&str]) -> bool {
67        if let Some(source) = &self.inner.source {
68            let display = source.to_string();
69            let debug = format!("{source:?}");
70            return markers
71                .iter()
72                .any(|marker| display.contains(marker) || debug.contains(marker));
73        }
74        false
75    }
76
77    fn description(&self) -> &str {
78        match &self.inner.kind {
79            Kind::Request => "request failed",
80            Kind::Conversion => "failed to convert between types",
81            Kind::ServerInfoChanged => "Ark server info changed while processing the request. Server info was refreshed, but the failed operation was not retried. Rebuild the request and retry if safe",
82        }
83    }
84}
85
86impl fmt::Debug for Error {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        let mut f = f.debug_tuple("ark_grpc::Error");
89
90        f.field(&self.inner.kind);
91
92        if let Some(source) = &self.inner.source {
93            f.field(source);
94        }
95
96        f.finish()
97    }
98}
99
100impl fmt::Display for Error {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.write_str(self.description())
103    }
104}
105
106impl StdError for Error {
107    fn source(&self) -> Option<&(dyn StdError + 'static)> {
108        self.inner
109            .source
110            .as_ref()
111            .map(|source| &**source as &(dyn StdError + 'static))
112    }
113}
114
115impl From<ConversionError> for Error {
116    fn from(value: ConversionError) -> Self {
117        Error::conversion(value)
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn is_version_mismatch_true_when_source_contains_marker() {
127        let err = Error::request("BUILD_VERSION_TOO_OLD: upgrade your client");
128        assert!(err.is_version_mismatch());
129    }
130
131    #[test]
132    fn is_version_mismatch_false_for_other_errors() {
133        let err = Error::request("connection refused");
134        assert!(!err.is_version_mismatch());
135    }
136
137    #[test]
138    fn is_version_mismatch_false_when_no_source() {
139        let err = Error::new(Kind::Request);
140        assert!(!err.is_version_mismatch());
141    }
142
143    #[test]
144    fn is_digest_mismatch_true_when_source_contains_marker() {
145        let err = Error::request("DIGEST_MISMATCH: invalid digest header");
146        assert!(err.is_digest_mismatch());
147    }
148
149    #[test]
150    fn is_digest_mismatch_true_when_generated_error_content_contains_marker() {
151        let source = crate::apis::Error::<()>::ResponseError(crate::apis::ResponseContent {
152            status: reqwest::StatusCode::PRECONDITION_FAILED,
153            content: "DIGEST_MISMATCH: invalid digest header".to_string(),
154            entity: None,
155        });
156        let err = Error::request(source);
157        assert!(err.is_digest_mismatch());
158    }
159
160    #[test]
161    fn is_digest_mismatch_false_for_other_errors() {
162        let err = Error::request("connection refused");
163        assert!(!err.is_digest_mismatch());
164    }
165
166    #[test]
167    fn is_server_info_changed_true_for_server_info_changed_kind() {
168        let err = Error::server_info_changed("DIGEST_MISMATCH");
169        assert!(err.is_server_info_changed());
170    }
171
172    #[test]
173    fn server_info_changed_is_not_classified_as_digest_mismatch() {
174        let err = Error::server_info_changed(Error::request("DIGEST_MISMATCH"));
175        assert!(err.is_server_info_changed());
176        assert!(!err.is_digest_mismatch());
177    }
178
179    #[test]
180    fn is_server_info_changed_false_for_other_errors() {
181        let err = Error::request("DIGEST_MISMATCH");
182        assert!(!err.is_server_info_changed());
183    }
184}