Skip to main content

ark_grpc/
error.rs

1use std::error::Error as StdError;
2use std::fmt;
3
4type Source = Box<dyn StdError + Send + Sync + 'static>;
5
6pub struct Error {
7    inner: ErrorImpl,
8}
9
10struct ErrorImpl {
11    kind: Kind,
12    source: Option<Source>,
13}
14
15#[derive(Debug)]
16enum Kind {
17    Connect,
18    NotConnected,
19    Request,
20    Conversion,
21    EventStreamDisconnect,
22    EventStream,
23    ServerInfoChanged,
24}
25
26impl Error {
27    fn new(kind: Kind) -> Self {
28        Self {
29            inner: ErrorImpl { kind, source: None },
30        }
31    }
32
33    pub(crate) fn with(mut self, source: impl Into<Source>) -> Self {
34        self.inner.source = Some(source.into());
35        self
36    }
37
38    pub(crate) fn connect(source: impl Into<Source>) -> Self {
39        Error::new(Kind::Connect).with(source)
40    }
41
42    pub(crate) fn not_connected() -> Self {
43        Error::new(Kind::NotConnected)
44    }
45
46    pub(crate) fn request(source: impl Into<Source>) -> Self {
47        Error::new(Kind::Request).with(source)
48    }
49
50    pub(crate) fn conversion(source: impl Into<Source>) -> Self {
51        Error::new(Kind::Conversion).with(source)
52    }
53
54    pub(crate) fn event_stream_disconnect() -> Self {
55        Error::new(Kind::EventStreamDisconnect)
56    }
57
58    pub(crate) fn event_stream(source: impl Into<Source>) -> Self {
59        Error::new(Kind::EventStream).with(source)
60    }
61
62    pub(crate) fn server_info_changed(source: impl Into<Source>) -> Self {
63        Error::new(Kind::ServerInfoChanged).with(source)
64    }
65
66    pub fn is_server_info_changed(&self) -> bool {
67        matches!(self.inner.kind, Kind::ServerInfoChanged)
68    }
69
70    /// Returns `true` if the server rejected the request because the SDK
71    /// version is too old.
72    pub fn is_version_mismatch(&self) -> bool {
73        if let Some(source) = &self.inner.source {
74            if let Some(status) = source.downcast_ref::<tonic::Status>() {
75                return status.code() == tonic::Code::FailedPrecondition
76                    && status.message().contains("BUILD_VERSION_TOO_OLD");
77            }
78        }
79        false
80    }
81
82    /// Returns `true` if the server rejected the request because the cached
83    /// `/info` digest is stale.
84    pub(crate) fn is_digest_mismatch(&self) -> bool {
85        if let Some(source) = &self.inner.source {
86            if let Some(status) = source.downcast_ref::<tonic::Status>() {
87                return status.code() == tonic::Code::FailedPrecondition
88                    && (status.message().contains("DIGEST_MISMATCH")
89                        || status.message().contains("invalid digest header"));
90            }
91        }
92        false
93    }
94
95    fn description(&self) -> &str {
96        match &self.inner.kind {
97            Kind::Connect => "failed to connect to Ark server",
98            Kind::NotConnected => "no connection to Ark server",
99            Kind::Request => "request failed",
100            Kind::Conversion => "failed to convert between types",
101            Kind::EventStreamDisconnect => "got disconnected from event stream",
102            Kind::EventStream => "error via event stream",
103            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",
104        }
105    }
106}
107
108impl fmt::Debug for Error {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        let mut f = f.debug_tuple("ark_grpc::Error");
111
112        f.field(&self.inner.kind);
113
114        if let Some(source) = &self.inner.source {
115            f.field(source);
116        }
117
118        f.finish()
119    }
120}
121
122impl fmt::Display for Error {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        f.write_str(self.description())?;
125        if let Some(source) = self.source() {
126            f.write_str(&source.to_string())?;
127        }
128
129        Ok(())
130    }
131}
132
133impl StdError for Error {
134    fn source(&self) -> Option<&(dyn StdError + 'static)> {
135        self.inner
136            .source
137            .as_ref()
138            .map(|source| &**source as &(dyn StdError + 'static))
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn is_version_mismatch_true_for_matching_status() {
148        let status = tonic::Status::failed_precondition("BUILD_VERSION_TOO_OLD");
149        let err = Error::request(status);
150        assert!(err.is_version_mismatch());
151    }
152
153    #[test]
154    fn is_version_mismatch_false_for_other_failed_precondition() {
155        let status = tonic::Status::failed_precondition("something else");
156        let err = Error::request(status);
157        assert!(!err.is_version_mismatch());
158    }
159
160    #[test]
161    fn is_version_mismatch_false_for_other_code() {
162        let status = tonic::Status::internal("BUILD_VERSION_TOO_OLD");
163        let err = Error::request(status);
164        assert!(!err.is_version_mismatch());
165    }
166
167    #[test]
168    fn is_version_mismatch_false_for_non_tonic_error() {
169        let err = Error::request("some string error");
170        assert!(!err.is_version_mismatch());
171    }
172
173    #[test]
174    fn is_version_mismatch_false_when_no_source() {
175        let err = Error::not_connected();
176        assert!(!err.is_version_mismatch());
177    }
178
179    #[test]
180    fn is_digest_mismatch_true_for_matching_status() {
181        let status = tonic::Status::failed_precondition("invalid digest header");
182        let err = Error::request(status);
183        assert!(err.is_digest_mismatch());
184    }
185
186    #[test]
187    fn is_digest_mismatch_true_for_error_code_in_message() {
188        let status = tonic::Status::failed_precondition("DIGEST_MISMATCH");
189        let err = Error::request(status);
190        assert!(err.is_digest_mismatch());
191    }
192
193    #[test]
194    fn is_digest_mismatch_false_for_other_failed_precondition() {
195        let status = tonic::Status::failed_precondition("something else");
196        let err = Error::request(status);
197        assert!(!err.is_digest_mismatch());
198    }
199}