xmtp_proto/traits/
error.rs1use std::fmt::Display;
2
3use crate::{ApiEndpoint, ProtoError};
4use thiserror::Error;
5use xmtp_common::{BoxDynError, ErrorCode, RetryableError, retryable};
6
7#[derive(Clone, Copy, Debug, Error, ErrorCode)]
10pub enum AuthError {
11 #[error("credential rejected")]
13 CredentialRejected { retryable: bool },
14 #[error("auth callback failed")]
16 CallbackFailed { retryable: bool },
17 #[error("auth attempts exhausted")]
19 Exhausted,
20 #[error("auth attempts exhausted")]
23 #[error_code("AuthError::Exhausted")]
24 ExhaustedAfterAttempt,
25 #[error("auth credential missing")]
27 MissingCredential,
28}
29
30impl RetryableError for AuthError {
31 fn is_retryable(&self) -> bool {
32 match self {
33 Self::CredentialRejected { retryable } | Self::CallbackFailed { retryable } => {
34 *retryable
35 }
36 Self::Exhausted | Self::ExhaustedAfterAttempt | Self::MissingCredential => false,
37 }
38 }
39}
40
41impl AuthError {
42 pub fn is_locked_out(&self) -> bool {
47 matches!(self, Self::Exhausted | Self::ExhaustedAfterAttempt)
48 }
49}
50
51impl ApiClientError {
52 pub fn is_locked_out(&self) -> bool {
56 matches!(self, Self::Auth(auth) if auth.is_locked_out())
57 }
58}
59
60#[derive(Debug, Error, ErrorCode)]
61#[non_exhaustive]
62pub enum ApiClientError {
63 #[error(transparent)]
64 #[error_code(inherit)]
65 Auth(#[from] AuthError),
66 #[error("api client at endpoint \"{}\" has error {}", endpoint, source)]
68 ClientWithEndpoint {
69 endpoint: String,
70 source: NetworkError,
72 },
73 #[error("client errored {}", source)]
75 Client { source: NetworkError },
76 #[error(transparent)]
78 Http(#[from] http::Error),
79 #[error(transparent)]
81 Body(#[from] BodyError),
82 #[error(transparent)]
84 DecodeError(#[from] prost::DecodeError),
85 #[error(transparent)]
87 Conversion(#[from] crate::ConversionError),
88 #[error(transparent)]
90 ProtoError(#[from] ProtoError),
91 #[error(transparent)]
93 InvalidUri(#[from] http::uri::InvalidUri),
94 #[error(transparent)]
96 Expired(#[from] xmtp_common::time::Expired),
97 #[error("{0}")]
99 Other(Box<dyn RetryableError>),
100 #[error("{0}")]
102 OtherUnretryable(BoxDynError),
103 #[error("Writes are disabled on this client.")]
105 WritesDisabled,
106}
107
108#[derive(Debug)]
112pub struct NetworkError {
113 source: Box<dyn RetryableError>,
114}
115
116impl std::error::Error for NetworkError {
117 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
118 Some(self.source.as_ref())
119 }
120}
121
122impl Display for NetworkError {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 write!(f, "{}", self.source)
125 }
126}
127
128impl RetryableError for NetworkError {
129 fn is_retryable(&self) -> bool {
130 self.source.is_retryable()
131 }
132
133 fn rate_limit_delay(&self) -> Option<xmtp_common::time::Duration> {
134 self.source.rate_limit_delay()
135 }
136}
137
138impl NetworkError {
139 pub fn new(e: impl RetryableError + 'static) -> Self {
140 NetworkError {
141 source: Box::new(e),
142 }
143 }
144}
145
146impl ApiClientError {
147 pub fn new(endpoint: ApiEndpoint, source: impl RetryableError + 'static) -> Self {
148 Self::ClientWithEndpoint {
149 endpoint: endpoint.to_string(),
150 source: NetworkError::new(source),
151 }
152 }
153
154 pub fn endpoint(self, endpoint: impl ToString) -> Self {
156 match self {
157 Self::Client { source } => Self::ClientWithEndpoint {
158 source,
159 endpoint: endpoint.to_string(),
160 },
161 v => v,
162 }
163 }
164
165 pub fn client(client: impl RetryableError + 'static) -> Self {
166 Self::Client {
167 source: NetworkError::new(client),
168 }
169 }
170
171 pub fn network_error(&self) -> Option<&NetworkError> {
174 use ApiClientError::*;
175 match self {
176 ClientWithEndpoint { source, .. } | Client { source, .. } => Some(source),
177 _ => None,
178 }
179 }
180}
181
182impl ApiClientError {
183 pub fn other<R: RetryableError + 'static>(e: R) -> Self {
184 ApiClientError::Other(Box::new(e))
185 }
186}
187
188impl RetryableError for ApiClientError {
189 fn is_retryable(&self) -> bool {
190 use ApiClientError::*;
191 match self {
192 Client { source } => retryable!(*source),
193 ClientWithEndpoint { source, .. } => retryable!(source),
194 Auth(e) => retryable!(e),
195 Body(e) => retryable!(e),
196 Http(_) => false,
197 DecodeError(_) => false,
198 Conversion(_) => false,
199 ProtoError(_) => false,
200 InvalidUri(_) => false,
201 Expired(_) => true,
202 Other(r) => retryable!(r),
203 OtherUnretryable(_) => false,
204 WritesDisabled => false,
205 }
206 }
207
208 fn rate_limit_delay(&self) -> Option<xmtp_common::time::Duration> {
209 match self {
210 Self::Client { source } | Self::ClientWithEndpoint { source, .. } => {
211 source.rate_limit_delay()
212 }
213 Self::Other(error) => error.rate_limit_delay(),
214 _ => None,
215 }
216 }
217}
218
219impl From<std::convert::Infallible> for ApiClientError {
221 fn from(_v: std::convert::Infallible) -> ApiClientError {
222 unreachable!("Infallible errors can never occur")
223 }
224}
225
226#[derive(Debug, Error)]
227pub enum BodyError {
228 #[error(transparent)]
229 UninitializedField(#[from] derive_builder::UninitializedFieldError),
230 #[error(transparent)]
231 Conversion(#[from] crate::ConversionError),
232}
233
234impl RetryableError for BodyError {
235 fn is_retryable(&self) -> bool {
236 false
237 }
238}
239
240pub fn grpc_status<'a>(
242 mut error: &'a (dyn std::error::Error + 'static),
243) -> Option<&'a tonic::Status> {
244 loop {
245 if let Some(status) = error.downcast_ref::<tonic::Status>() {
246 return Some(status);
247 }
248 error = match error.downcast_ref::<ApiClientError>() {
249 Some(ApiClientError::Other(inner)) => inner.as_ref(),
250 Some(ApiClientError::OtherUnretryable(inner)) => inner.as_ref(),
251 _ => error.source()?,
252 };
253 }
254}