modio/
error.rs

1//! Client errors
2use std::error::Error as StdError;
3use std::fmt;
4use std::time::Duration;
5
6use reqwest::StatusCode;
7
8use crate::types::Error as ApiError;
9
10/// A `Result` alias where the `Err` case is `modio::Error`.
11pub type Result<T, E = Error> = std::result::Result<T, E>;
12
13/// The Errors that may occur when using `Modio`.
14pub struct Error {
15    inner: Box<Inner>,
16}
17
18type BoxError = Box<dyn StdError + Send + Sync>;
19
20struct Inner {
21    kind: Kind,
22    error_ref: Option<u16>,
23    source: Option<BoxError>,
24}
25
26impl Error {
27    #[inline]
28    pub(crate) fn new(kind: Kind) -> Self {
29        Self {
30            inner: Box::new(Inner {
31                kind,
32                error_ref: None,
33                source: None,
34            }),
35        }
36    }
37
38    #[inline]
39    pub(crate) fn with<E: Into<BoxError>>(mut self, source: E) -> Self {
40        self.inner.source = Some(source.into());
41        self
42    }
43
44    #[inline]
45    pub(crate) fn with_error_ref(mut self, error_ref: u16) -> Self {
46        self.inner.error_ref = Some(error_ref);
47        self
48    }
49
50    /// Returns true if the API key/access token is incorrect, revoked, expired or the request
51    /// needs a different authentication method.
52    pub fn is_auth(&self) -> bool {
53        matches!(self.inner.kind, Kind::Unauthorized | Kind::TokenRequired)
54    }
55
56    /// Returns true if the acceptance of the Terms of Use is required before continuing external
57    /// authorization.
58    pub fn is_terms_acceptance_required(&self) -> bool {
59        matches!(self.inner.kind, Kind::TermsAcceptanceRequired)
60    }
61
62    /// Returns true if the error is from a type Builder.
63    pub fn is_builder(&self) -> bool {
64        matches!(self.inner.kind, Kind::Builder)
65    }
66
67    /// Returns true if the error is from a [`DownloadAction`](crate::download::DownloadAction).
68    pub fn is_download(&self) -> bool {
69        matches!(self.inner.kind, Kind::Download)
70    }
71
72    /// Returns true if the rate limit associated with credentials has been exhausted.
73    pub fn is_ratelimited(&self) -> bool {
74        matches!(self.inner.kind, Kind::RateLimit { .. })
75    }
76
77    /// Returns true if the error was generated from a response.
78    pub fn is_response(&self) -> bool {
79        matches!(self.inner.kind, Kind::Response { .. })
80    }
81
82    /// Returns true if the error contains validation errors.
83    pub fn is_validation(&self) -> bool {
84        matches!(self.inner.kind, Kind::Validation { .. })
85    }
86
87    /// Returns true if the error is related to serialization.
88    pub fn is_decode(&self) -> bool {
89        matches!(self.inner.kind, Kind::Decode)
90    }
91
92    /// Returns the API error if the error was generated from a response.
93    pub fn api_error(&self) -> Option<&ApiError> {
94        match &self.inner.kind {
95            Kind::Response { error, .. } => Some(error),
96            _ => None,
97        }
98    }
99
100    /// Returns modio's error reference code.
101    ///
102    /// See the [Error Codes](https://docs.mod.io/restapiref/#error-codes) docs for more information.
103    pub fn error_ref(&self) -> Option<u16> {
104        self.inner.error_ref
105    }
106
107    /// Returns status code if the error was generated from a response.
108    pub fn status(&self) -> Option<StatusCode> {
109        match self.inner.kind {
110            Kind::Response { status, .. } => Some(status),
111            _ => None,
112        }
113    }
114
115    /// Returns validation message & errors from the response.
116    pub fn validation(&self) -> Option<(&String, &Vec<(String, String)>)> {
117        match self.inner.kind {
118            Kind::Validation {
119                ref message,
120                ref errors,
121            } => Some((message, errors)),
122            _ => None,
123        }
124    }
125}
126
127impl fmt::Debug for Error {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        let mut builder = f.debug_struct("modio::Error");
130
131        builder.field("kind", &self.inner.kind);
132        if let Some(ref error_ref) = self.inner.error_ref {
133            builder.field("error_ref", error_ref);
134        }
135
136        if let Some(ref source) = self.inner.source {
137            builder.field("source", source);
138        }
139        builder.finish()
140    }
141}
142
143impl fmt::Display for Error {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self.inner.kind {
146            Kind::Unauthorized => f.write_str("unauthorized")?,
147            Kind::TokenRequired => f.write_str("access token is required")?,
148            Kind::TermsAcceptanceRequired => f.write_str("terms acceptance is required")?,
149            Kind::Builder => f.write_str("builder error")?,
150            Kind::Decode => f.write_str("error decoding response body")?,
151            Kind::Download => f.write_str("download error")?,
152            Kind::Request => f.write_str("http request error")?,
153            Kind::Response { status, .. } => {
154                let prefix = if status.is_client_error() {
155                    "HTTP status client error"
156                } else {
157                    debug_assert!(status.is_server_error());
158                    "HTTP status server error"
159                };
160                write!(f, "{prefix} ({status})")?;
161            }
162            Kind::RateLimit { retry_after } => {
163                write!(f, "API rate limit reached. Try again in {retry_after:?}.")?;
164            }
165            Kind::Validation {
166                ref message,
167                ref errors,
168            } => {
169                write!(f, "validation failed: '{message}' {errors:?}")?;
170            }
171        };
172        if let Some(ref e) = self.inner.source {
173            write!(f, ": {e}")?;
174        }
175        Ok(())
176    }
177}
178
179impl StdError for Error {
180    fn source(&self) -> Option<&(dyn StdError + 'static)> {
181        self.inner.source.as_ref().map(|e| &**e as _)
182    }
183}
184
185#[derive(Debug)]
186pub(crate) enum Kind {
187    /// API key/access token is incorrect, revoked or expired.
188    Unauthorized,
189    /// Access token is required to perform the action.
190    TokenRequired,
191    /// The acceptance of the Terms of Use is required.
192    TermsAcceptanceRequired,
193    Download,
194    Validation {
195        message: String,
196        errors: Vec<(String, String)>,
197    },
198    RateLimit {
199        retry_after: Duration,
200    },
201    Builder,
202    Request,
203    Response {
204        status: StatusCode,
205        error: ApiError,
206    },
207    Decode,
208}
209
210pub(crate) fn token_required() -> Error {
211    Error::new(Kind::TokenRequired)
212}
213
214pub(crate) fn builder_or_request(e: reqwest::Error) -> Error {
215    if e.is_builder() {
216        builder(e)
217    } else {
218        request(e)
219    }
220}
221
222pub(crate) fn builder<E: Into<BoxError>>(source: E) -> Error {
223    Error::new(Kind::Builder).with(source)
224}
225
226pub(crate) fn request<E: Into<BoxError>>(source: E) -> Error {
227    Error::new(Kind::Request).with(source)
228}
229
230pub(crate) fn decode<E: Into<BoxError>>(source: E) -> Error {
231    Error::new(Kind::Decode).with(source)
232}
233
234pub(crate) fn error_for_status(status: StatusCode, error: ApiError) -> Error {
235    let error_ref = error.error_ref;
236    let kind = match status {
237        StatusCode::UNPROCESSABLE_ENTITY => Kind::Validation {
238            message: error.message,
239            errors: error.errors,
240        },
241        StatusCode::UNAUTHORIZED => Kind::Unauthorized,
242        StatusCode::FORBIDDEN if error_ref == 11051 => Kind::TermsAcceptanceRequired,
243        _ => Kind::Response { status, error },
244    };
245    Error::new(kind).with_error_ref(error_ref)
246}
247
248pub(crate) fn ratelimit(retry_after: u64) -> Error {
249    Error::new(Kind::RateLimit {
250        retry_after: Duration::from_secs(retry_after),
251    })
252}
253
254pub(crate) fn download<E: Into<BoxError>>(source: E) -> Error {
255    Error::new(Kind::Download).with(source)
256}