matrix_sdk/
error.rs

1// Copyright 2020 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Error conditions.
16
17use std::{io::Error as IoError, sync::Arc, time::Duration};
18
19use as_variant::as_variant;
20use http::StatusCode;
21#[cfg(feature = "qrcode")]
22use matrix_sdk_base::crypto::ScanError;
23#[cfg(feature = "e2e-encryption")]
24use matrix_sdk_base::crypto::{
25    CryptoStoreError, DecryptorError, KeyExportError, MegolmError, OlmError,
26};
27use matrix_sdk_base::{
28    event_cache::store::EventCacheStoreError, Error as SdkBaseError, QueueWedgeError, RoomState,
29    StoreError,
30};
31use reqwest::Error as ReqwestError;
32use ruma::{
33    api::{
34        client::{
35            error::{ErrorBody, ErrorKind, RetryAfter},
36            uiaa::{UiaaInfo, UiaaResponse},
37        },
38        error::{FromHttpResponseError, IntoHttpError},
39    },
40    events::tag::InvalidUserTagName,
41    push::{InsertPushRuleError, RemovePushRuleError},
42    IdParseError,
43};
44use serde_json::Error as JsonError;
45use thiserror::Error;
46use url::ParseError as UrlParseError;
47
48use crate::{event_cache::EventCacheError, media::MediaError, store_locks::LockStoreError};
49
50/// Result type of the matrix-sdk.
51pub type Result<T, E = Error> = std::result::Result<T, E>;
52
53/// Result type of a pure HTTP request.
54pub type HttpResult<T> = std::result::Result<T, HttpError>;
55
56/// An error response from a Matrix API call, using a client API specific
57/// representation if the endpoint is from that.
58#[derive(Error, Debug)]
59pub enum RumaApiError {
60    /// A client API response error.
61    #[error(transparent)]
62    ClientApi(ruma::api::client::Error),
63
64    /// A user-interactive authentication API error.
65    ///
66    /// When registering or authenticating, the Matrix server can send a
67    /// `UiaaInfo` as the error type, this is a User-Interactive Authentication
68    /// API response. This represents an error with information about how to
69    /// authenticate the user.
70    #[error("User-Interactive Authentication required.")]
71    Uiaa(UiaaInfo),
72
73    /// Another API response error.
74    #[error(transparent)]
75    Other(ruma::api::error::MatrixError),
76}
77
78impl RumaApiError {
79    /// If `self` is `ClientApi(e)`, returns `Some(e)`.
80    ///
81    /// Otherwise, returns `None`.
82    pub fn as_client_api_error(&self) -> Option<&ruma::api::client::Error> {
83        as_variant!(self, Self::ClientApi)
84    }
85}
86
87/// An HTTP error, representing either a connection error or an error while
88/// converting the raw HTTP response into a Matrix response.
89#[derive(Error, Debug)]
90pub enum HttpError {
91    /// Error at the HTTP layer.
92    #[error(transparent)]
93    Reqwest(#[from] ReqwestError),
94
95    /// Queried endpoint is not meant for clients.
96    #[error("the queried endpoint is not meant for clients")]
97    NotClientRequest,
98
99    /// API response error (deserialization, or a Matrix-specific error).
100    #[error(transparent)]
101    Api(#[from] FromHttpResponseError<RumaApiError>),
102
103    /// Error when creating an API request (e.g. serialization of
104    /// body/headers/query parameters).
105    #[error(transparent)]
106    IntoHttp(IntoHttpError),
107
108    /// Error while refreshing the access token.
109    #[error(transparent)]
110    RefreshToken(RefreshTokenError),
111}
112
113#[rustfmt::skip] // stop rustfmt breaking the `<code>` in docs across multiple lines
114impl HttpError {
115    /// If `self` is
116    /// <code>[Api](Self::Api)([Server](FromHttpResponseError::Server)(e))</code>,
117    /// returns `Some(e)`.
118    ///
119    /// Otherwise, returns `None`.
120    pub fn as_ruma_api_error(&self) -> Option<&RumaApiError> {
121        as_variant!(self, Self::Api(FromHttpResponseError::Server(e)) => e)
122    }
123
124    /// Shorthand for
125    /// <code>.[as_ruma_api_error](Self::as_ruma_api_error)().[and_then](Option::and_then)([RumaApiError::as_client_api_error])</code>.
126    pub fn as_client_api_error(&self) -> Option<&ruma::api::client::Error> {
127        self.as_ruma_api_error().and_then(RumaApiError::as_client_api_error)
128    }
129}
130
131// Another impl block that's formatted with rustfmt.
132impl HttpError {
133    /// If `self` is a server error in the `errcode` + `error` format expected
134    /// for client-API endpoints, returns the error kind (`errcode`).
135    pub fn client_api_error_kind(&self) -> Option<&ErrorKind> {
136        self.as_client_api_error()
137            .and_then(|e| as_variant!(&e.body, ErrorBody::Standard { kind, .. } => kind))
138    }
139
140    /// Try to destructure the error into an universal interactive auth info.
141    ///
142    /// Some requests require universal interactive auth, doing such a request
143    /// will always fail the first time with a 401 status code, the response
144    /// body will contain info how the client can authenticate.
145    ///
146    /// The request will need to be retried, this time containing additional
147    /// authentication data.
148    ///
149    /// This method is an convenience method to get to the info the server
150    /// returned on the first, failed request.
151    pub fn as_uiaa_response(&self) -> Option<&UiaaInfo> {
152        self.as_ruma_api_error().and_then(as_variant!(RumaApiError::Uiaa))
153    }
154
155    /// Returns whether an HTTP error response should be qualified as transient
156    /// or permanent.
157    pub(crate) fn retry_kind(&self) -> RetryKind {
158        match self {
159            // If it was a plain network error, it's either that we're disconnected from the
160            // internet, or that the remote is, so retry a few times.
161            HttpError::Reqwest(_) => RetryKind::NetworkFailure,
162
163            HttpError::Api(FromHttpResponseError::Server(api_error)) => {
164                RetryKind::from_api_error(api_error)
165            }
166            _ => RetryKind::Permanent,
167        }
168    }
169}
170
171/// How should we behave with respect to retry behavior after an [`HttpError`]
172/// happened?
173pub(crate) enum RetryKind {
174    /// The request failed because of an error at the network layer.
175    NetworkFailure,
176
177    /// The request failed with a "transient" error, meaning it could be retried
178    /// either soon, or after a given amount of time expressed in
179    /// `retry_after`.
180    Transient {
181        // This is used only for attempts to retry, so on non-wasm32 code (in the `native` module).
182        #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
183        retry_after: Option<Duration>,
184    },
185
186    /// The request failed with a non-transient error, and retrying it would
187    /// likely cause the same error again, so it's not worth retrying.
188    Permanent,
189}
190
191impl RetryKind {
192    /// Construct a [`RetryKind`] from a Ruma API error.
193    ///
194    /// The Ruma API error is for errors which have the standard error response
195    /// format defined in the [spec].
196    ///
197    /// [spec]: https://spec.matrix.org/v1.11/client-server-api/#standard-error-response
198    fn from_api_error(api_error: &RumaApiError) -> Self {
199        use ruma::api::client::Error;
200
201        match api_error {
202            RumaApiError::ClientApi(client_error) => {
203                let Error { status_code, body, .. } = client_error;
204
205                match body {
206                    ErrorBody::Standard { kind, .. } => match kind {
207                        ErrorKind::LimitExceeded { retry_after } => {
208                            RetryKind::from_retry_after(retry_after.as_ref())
209                        }
210                        ErrorKind::Unrecognized => RetryKind::Permanent,
211                        _ => RetryKind::from_status_code(*status_code),
212                    },
213                    _ => RetryKind::from_status_code(*status_code),
214                }
215            }
216            RumaApiError::Other(e) => RetryKind::from_status_code(e.status_code),
217            RumaApiError::Uiaa(_) => RetryKind::Permanent,
218        }
219    }
220
221    /// Create a [`RetryKind`] if we have found a [`RetryAfter`] defined in an
222    /// error.
223    ///
224    /// This method should be used for errors where the server explicitly tells
225    /// us how long we must wait before we retry the request again.
226    fn from_retry_after(retry_after: Option<&RetryAfter>) -> Self {
227        let retry_after = retry_after
228            .and_then(|retry_after| match retry_after {
229                RetryAfter::Delay(d) => Some(d),
230                RetryAfter::DateTime(_) => None,
231            })
232            .copied();
233
234        Self::Transient { retry_after }
235    }
236
237    /// Construct a [`RetryKind`] from a HTTP [`StatusCode`].
238    ///
239    /// This should be used if we don't have a more specific Matrix style error
240    /// which gives us more information about the nature of the error, i.e.
241    /// if we received an error from a reverse proxy while the Matrix
242    /// homeserver is down.
243    fn from_status_code(status_code: StatusCode) -> Self {
244        // If the status code is 429, this is requesting a retry in HTTP, without the
245        // custom `errcode`. Treat that as a retriable request with no specified
246        // retry_after delay.
247        if status_code == StatusCode::TOO_MANY_REQUESTS || status_code.is_server_error() {
248            RetryKind::Transient { retry_after: None }
249        } else {
250            RetryKind::Permanent
251        }
252    }
253}
254
255/// Internal representation of errors.
256#[derive(Error, Debug)]
257#[non_exhaustive]
258pub enum Error {
259    /// Error doing an HTTP request.
260    #[error(transparent)]
261    Http(#[from] HttpError),
262
263    /// Queried endpoint requires authentication but was called on an anonymous
264    /// client.
265    #[error("the queried endpoint requires authentication but was called before logging in")]
266    AuthenticationRequired,
267
268    /// This request failed because the local data wasn't sufficient.
269    #[error("Local cache doesn't contain all necessary data to perform the action.")]
270    InsufficientData,
271
272    /// Attempting to restore a session after the olm-machine has already been
273    /// set up fails
274    #[cfg(feature = "e2e-encryption")]
275    #[error("The olm machine has already been initialized")]
276    BadCryptoStoreState,
277
278    /// Attempting to access the olm-machine but it is not yet available.
279    #[cfg(feature = "e2e-encryption")]
280    #[error("The olm machine isn't yet available")]
281    NoOlmMachine,
282
283    /// An error de/serializing type for the `StateStore`
284    #[error(transparent)]
285    SerdeJson(#[from] JsonError),
286
287    /// An IO error happened.
288    #[error(transparent)]
289    Io(#[from] IoError),
290
291    /// An error occurred in the crypto store.
292    #[cfg(feature = "e2e-encryption")]
293    #[error(transparent)]
294    CryptoStoreError(#[from] CryptoStoreError),
295
296    /// An error occurred with a cross-process store lock.
297    #[error(transparent)]
298    CrossProcessLockError(#[from] LockStoreError),
299
300    /// An error occurred during a E2EE operation.
301    #[cfg(feature = "e2e-encryption")]
302    #[error(transparent)]
303    OlmError(#[from] OlmError),
304
305    /// An error occurred during a E2EE group operation.
306    #[cfg(feature = "e2e-encryption")]
307    #[error(transparent)]
308    MegolmError(#[from] MegolmError),
309
310    /// An error occurred during decryption.
311    #[cfg(feature = "e2e-encryption")]
312    #[error(transparent)]
313    DecryptorError(#[from] DecryptorError),
314
315    /// An error occurred in the state store.
316    #[error(transparent)]
317    StateStore(#[from] StoreError),
318
319    /// An error occurred in the event cache store.
320    #[error(transparent)]
321    EventCacheStore(#[from] EventCacheStoreError),
322
323    /// An error encountered when trying to parse an identifier.
324    #[error(transparent)]
325    Identifier(#[from] IdParseError),
326
327    /// An error encountered when trying to parse a url.
328    #[error(transparent)]
329    Url(#[from] UrlParseError),
330
331    /// An error while scanning a QR code.
332    #[cfg(feature = "qrcode")]
333    #[error(transparent)]
334    QrCodeScanError(#[from] ScanError),
335
336    /// An error encountered when trying to parse a user tag name.
337    #[error(transparent)]
338    UserTagName(#[from] InvalidUserTagName),
339
340    /// An error occurred within sliding-sync
341    #[error(transparent)]
342    SlidingSync(#[from] crate::sliding_sync::Error),
343
344    /// Attempted to call a method on a room that requires the user to have a
345    /// specific membership state in the room, but the membership state is
346    /// different.
347    #[error("wrong room state: {0}")]
348    WrongRoomState(WrongRoomState),
349
350    /// Session callbacks have been set multiple times.
351    #[error("session callbacks have been set multiple times")]
352    MultipleSessionCallbacks,
353
354    /// An error occurred interacting with the OAuth 2.0 API.
355    #[error(transparent)]
356    OAuth(#[from] crate::authentication::oauth::OAuthError),
357
358    /// A concurrent request to a deduplicated request has failed.
359    #[error("a concurrent request failed; see logs for details")]
360    ConcurrentRequestFailed,
361
362    /// An other error was raised.
363    ///
364    /// This might happen because encryption was enabled on the base-crate
365    /// but not here and that raised.
366    #[error("unknown error: {0}")]
367    UnknownError(Box<dyn std::error::Error + Send + Sync>),
368
369    /// An error coming from the event cache subsystem.
370    #[error(transparent)]
371    EventCache(#[from] EventCacheError),
372
373    /// An item has been wedged in the send queue.
374    #[error(transparent)]
375    SendQueueWedgeError(#[from] QueueWedgeError),
376
377    /// Backups are not enabled
378    #[error("backups are not enabled")]
379    BackupNotEnabled,
380
381    /// An error happened during handling of a media subrequest.
382    #[error(transparent)]
383    Media(#[from] MediaError),
384}
385
386#[rustfmt::skip] // stop rustfmt breaking the `<code>` in docs across multiple lines
387impl Error {
388    /// If `self` is
389    /// <code>[Http](Self::Http)([Api](HttpError::Api)([Server](FromHttpResponseError::Server)(e)))</code>,
390    /// returns `Some(e)`.
391    ///
392    /// Otherwise, returns `None`.
393    pub fn as_ruma_api_error(&self) -> Option<&RumaApiError> {
394        as_variant!(self, Self::Http).and_then(|e| e.as_ruma_api_error())
395    }
396
397    /// Shorthand for
398    /// <code>.[as_ruma_api_error](Self::as_ruma_api_error)().[and_then](Option::and_then)([RumaApiError::as_client_api_error])</code>.
399    pub fn as_client_api_error(&self) -> Option<&ruma::api::client::Error> {
400        self.as_ruma_api_error().and_then(RumaApiError::as_client_api_error)
401    }
402
403    /// If `self` is a server error in the `errcode` + `error` format expected
404    /// for client-API endpoints, returns the error kind (`errcode`).
405    pub fn client_api_error_kind(&self) -> Option<&ErrorKind> {
406        self.as_client_api_error().and_then(|e| {
407            as_variant!(&e.body, ErrorBody::Standard { kind, .. } => kind)
408        })
409    }
410
411    /// Try to destructure the error into an universal interactive auth info.
412    ///
413    /// Some requests require universal interactive auth, doing such a request
414    /// will always fail the first time with a 401 status code, the response
415    /// body will contain info how the client can authenticate.
416    ///
417    /// The request will need to be retried, this time containing additional
418    /// authentication data.
419    ///
420    /// This method is an convenience method to get to the info the server
421    /// returned on the first, failed request.
422    pub fn as_uiaa_response(&self) -> Option<&UiaaInfo> {
423        self.as_ruma_api_error().and_then(as_variant!(RumaApiError::Uiaa))
424    }
425}
426
427/// Error for the room key importing functionality.
428#[cfg(feature = "e2e-encryption")]
429#[derive(Error, Debug)]
430// This is allowed because key importing isn't enabled under wasm.
431#[allow(dead_code)]
432pub enum RoomKeyImportError {
433    /// An error de/serializing type for the `StateStore`
434    #[error(transparent)]
435    SerdeJson(#[from] JsonError),
436
437    /// The crypto store isn't yet open. Logging in is required to open the
438    /// crypto store.
439    #[error("The crypto store hasn't been yet opened, can't import yet.")]
440    StoreClosed,
441
442    /// An IO error happened.
443    #[error(transparent)]
444    Io(#[from] IoError),
445
446    /// An error occurred in the crypto store.
447    #[error(transparent)]
448    CryptoStore(#[from] CryptoStoreError),
449
450    /// An error occurred while importing the key export.
451    #[error(transparent)]
452    Export(#[from] KeyExportError),
453}
454
455impl From<FromHttpResponseError<ruma::api::client::Error>> for HttpError {
456    fn from(err: FromHttpResponseError<ruma::api::client::Error>) -> Self {
457        Self::Api(err.map(RumaApiError::ClientApi))
458    }
459}
460
461impl From<FromHttpResponseError<UiaaResponse>> for HttpError {
462    fn from(err: FromHttpResponseError<UiaaResponse>) -> Self {
463        Self::Api(err.map(|e| match e {
464            UiaaResponse::AuthResponse(i) => RumaApiError::Uiaa(i),
465            UiaaResponse::MatrixError(e) => RumaApiError::ClientApi(e),
466        }))
467    }
468}
469
470impl From<FromHttpResponseError<ruma::api::error::MatrixError>> for HttpError {
471    fn from(err: FromHttpResponseError<ruma::api::error::MatrixError>) -> Self {
472        Self::Api(err.map(RumaApiError::Other))
473    }
474}
475
476impl From<SdkBaseError> for Error {
477    fn from(e: SdkBaseError) -> Self {
478        match e {
479            SdkBaseError::StateStore(e) => Self::StateStore(e),
480            #[cfg(feature = "e2e-encryption")]
481            SdkBaseError::CryptoStore(e) => Self::CryptoStoreError(e),
482            #[cfg(feature = "e2e-encryption")]
483            SdkBaseError::BadCryptoStoreState => Self::BadCryptoStoreState,
484            #[cfg(feature = "e2e-encryption")]
485            SdkBaseError::OlmError(e) => Self::OlmError(e),
486            #[cfg(feature = "eyre")]
487            _ => Self::UnknownError(eyre::eyre!(e).into()),
488            #[cfg(all(not(feature = "eyre"), feature = "anyhow"))]
489            _ => Self::UnknownError(anyhow::anyhow!(e).into()),
490            #[cfg(all(not(feature = "eyre"), not(feature = "anyhow")))]
491            _ => {
492                let e: Box<dyn std::error::Error + Sync + Send> = format!("{e:?}").into();
493                Self::UnknownError(e)
494            }
495        }
496    }
497}
498
499impl From<ReqwestError> for Error {
500    fn from(e: ReqwestError) -> Self {
501        Error::Http(HttpError::Reqwest(e))
502    }
503}
504
505/// Errors that can happen when interacting with the beacon API.
506#[derive(Debug, Error)]
507pub enum BeaconError {
508    // A network error occurred.
509    #[error("Network error: {0}")]
510    Network(#[from] HttpError),
511
512    // The beacon information is not found.
513    #[error("Existing beacon information not found.")]
514    NotFound,
515
516    // The redacted event is not an error, but it's not useful for the client.
517    #[error("Beacon event is redacted and cannot be processed.")]
518    Redacted,
519
520    // The client must join the room to access the beacon information.
521    #[error("Must join the room to access beacon information.")]
522    Stripped,
523
524    // The beacon event could not be deserialized.
525    #[error("Deserialization error: {0}")]
526    Deserialization(#[from] serde_json::Error),
527
528    // The beacon event is expired.
529    #[error("The beacon event has expired.")]
530    NotLive,
531
532    // Allow for other errors to be wrapped.
533    #[error("Other error: {0}")]
534    Other(Box<Error>),
535}
536
537impl From<Error> for BeaconError {
538    fn from(err: Error) -> Self {
539        BeaconError::Other(Box::new(err))
540    }
541}
542
543/// Errors that can happen when refreshing an access token.
544///
545/// This is usually only returned by [`Client::refresh_access_token()`], unless
546/// [handling refresh tokens] is activated for the `Client`.
547///
548/// [`Client::refresh_access_token()`]: crate::Client::refresh_access_token()
549/// [handling refresh tokens]: crate::ClientBuilder::handle_refresh_tokens()
550#[derive(Debug, Error, Clone)]
551pub enum RefreshTokenError {
552    /// Tried to send a refresh token request without a refresh token.
553    #[error("missing refresh token")]
554    RefreshTokenRequired,
555
556    /// An error occurred interacting with the native Matrix authentication API.
557    #[error(transparent)]
558    MatrixAuth(Arc<HttpError>),
559
560    /// An error occurred interacting with the OAuth 2.0 API.
561    #[error(transparent)]
562    OAuth(#[from] Arc<crate::authentication::oauth::OAuthError>),
563}
564
565/// Errors that can occur when manipulating push notification settings.
566#[derive(Debug, Error, Clone, PartialEq)]
567pub enum NotificationSettingsError {
568    /// Invalid parameter.
569    #[error("Invalid parameter `{0}`")]
570    InvalidParameter(String),
571    /// Unable to add push rule.
572    #[error("Unable to add push rule")]
573    UnableToAddPushRule,
574    /// Unable to remove push rule.
575    #[error("Unable to remove push rule")]
576    UnableToRemovePushRule,
577    /// Unable to update push rule.
578    #[error("Unable to update push rule")]
579    UnableToUpdatePushRule,
580    /// Rule not found
581    #[error("Rule `{0}` not found")]
582    RuleNotFound(String),
583    /// Unable to save the push rules
584    #[error("Unable to save push rules")]
585    UnableToSavePushRules,
586}
587
588impl From<InsertPushRuleError> for NotificationSettingsError {
589    fn from(_: InsertPushRuleError) -> Self {
590        Self::UnableToAddPushRule
591    }
592}
593
594impl From<RemovePushRuleError> for NotificationSettingsError {
595    fn from(_: RemovePushRuleError) -> Self {
596        Self::UnableToRemovePushRule
597    }
598}
599
600#[derive(Debug, Error)]
601#[error("expected: {expected}, got: {got:?}")]
602pub struct WrongRoomState {
603    expected: &'static str,
604    got: RoomState,
605}
606
607impl WrongRoomState {
608    pub(crate) fn new(expected: &'static str, got: RoomState) -> Self {
609        Self { expected, got }
610    }
611}