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 OpenID Connect API.
355    #[cfg(feature = "experimental-oidc")]
356    #[error(transparent)]
357    Oidc(#[from] crate::authentication::oidc::OidcError),
358
359    /// A concurrent request to a deduplicated request has failed.
360    #[error("a concurrent request failed; see logs for details")]
361    ConcurrentRequestFailed,
362
363    /// An other error was raised.
364    ///
365    /// This might happen because encryption was enabled on the base-crate
366    /// but not here and that raised.
367    #[error("unknown error: {0}")]
368    UnknownError(Box<dyn std::error::Error + Send + Sync>),
369
370    /// An error coming from the event cache subsystem.
371    #[error(transparent)]
372    EventCache(#[from] EventCacheError),
373
374    /// An item has been wedged in the send queue.
375    #[error(transparent)]
376    SendQueueWedgeError(#[from] QueueWedgeError),
377
378    /// Backups are not enabled
379    #[error("backups are not enabled")]
380    BackupNotEnabled,
381
382    /// An error happened during handling of a media subrequest.
383    #[error(transparent)]
384    Media(#[from] MediaError),
385}
386
387#[rustfmt::skip] // stop rustfmt breaking the `<code>` in docs across multiple lines
388impl Error {
389    /// If `self` is
390    /// <code>[Http](Self::Http)([Api](HttpError::Api)([Server](FromHttpResponseError::Server)(e)))</code>,
391    /// returns `Some(e)`.
392    ///
393    /// Otherwise, returns `None`.
394    pub fn as_ruma_api_error(&self) -> Option<&RumaApiError> {
395        as_variant!(self, Self::Http).and_then(|e| e.as_ruma_api_error())
396    }
397
398    /// Shorthand for
399    /// <code>.[as_ruma_api_error](Self::as_ruma_api_error)().[and_then](Option::and_then)([RumaApiError::as_client_api_error])</code>.
400    pub fn as_client_api_error(&self) -> Option<&ruma::api::client::Error> {
401        self.as_ruma_api_error().and_then(RumaApiError::as_client_api_error)
402    }
403
404    /// If `self` is a server error in the `errcode` + `error` format expected
405    /// for client-API endpoints, returns the error kind (`errcode`).
406    pub fn client_api_error_kind(&self) -> Option<&ErrorKind> {
407        self.as_client_api_error().and_then(|e| {
408            as_variant!(&e.body, ErrorBody::Standard { kind, .. } => kind)
409        })
410    }
411
412    /// Try to destructure the error into an universal interactive auth info.
413    ///
414    /// Some requests require universal interactive auth, doing such a request
415    /// will always fail the first time with a 401 status code, the response
416    /// body will contain info how the client can authenticate.
417    ///
418    /// The request will need to be retried, this time containing additional
419    /// authentication data.
420    ///
421    /// This method is an convenience method to get to the info the server
422    /// returned on the first, failed request.
423    pub fn as_uiaa_response(&self) -> Option<&UiaaInfo> {
424        self.as_ruma_api_error().and_then(as_variant!(RumaApiError::Uiaa))
425    }
426}
427
428/// Error for the room key importing functionality.
429#[cfg(feature = "e2e-encryption")]
430#[derive(Error, Debug)]
431// This is allowed because key importing isn't enabled under wasm.
432#[allow(dead_code)]
433pub enum RoomKeyImportError {
434    /// An error de/serializing type for the `StateStore`
435    #[error(transparent)]
436    SerdeJson(#[from] JsonError),
437
438    /// The crypto store isn't yet open. Logging in is required to open the
439    /// crypto store.
440    #[error("The crypto store hasn't been yet opened, can't import yet.")]
441    StoreClosed,
442
443    /// An IO error happened.
444    #[error(transparent)]
445    Io(#[from] IoError),
446
447    /// An error occurred in the crypto store.
448    #[error(transparent)]
449    CryptoStore(#[from] CryptoStoreError),
450
451    /// An error occurred while importing the key export.
452    #[error(transparent)]
453    Export(#[from] KeyExportError),
454}
455
456impl From<FromHttpResponseError<ruma::api::client::Error>> for HttpError {
457    fn from(err: FromHttpResponseError<ruma::api::client::Error>) -> Self {
458        Self::Api(err.map(RumaApiError::ClientApi))
459    }
460}
461
462impl From<FromHttpResponseError<UiaaResponse>> for HttpError {
463    fn from(err: FromHttpResponseError<UiaaResponse>) -> Self {
464        Self::Api(err.map(|e| match e {
465            UiaaResponse::AuthResponse(i) => RumaApiError::Uiaa(i),
466            UiaaResponse::MatrixError(e) => RumaApiError::ClientApi(e),
467        }))
468    }
469}
470
471impl From<FromHttpResponseError<ruma::api::error::MatrixError>> for HttpError {
472    fn from(err: FromHttpResponseError<ruma::api::error::MatrixError>) -> Self {
473        Self::Api(err.map(RumaApiError::Other))
474    }
475}
476
477impl From<SdkBaseError> for Error {
478    fn from(e: SdkBaseError) -> Self {
479        match e {
480            SdkBaseError::StateStore(e) => Self::StateStore(e),
481            #[cfg(feature = "e2e-encryption")]
482            SdkBaseError::CryptoStore(e) => Self::CryptoStoreError(e),
483            #[cfg(feature = "e2e-encryption")]
484            SdkBaseError::BadCryptoStoreState => Self::BadCryptoStoreState,
485            #[cfg(feature = "e2e-encryption")]
486            SdkBaseError::OlmError(e) => Self::OlmError(e),
487            #[cfg(feature = "eyre")]
488            _ => Self::UnknownError(eyre::eyre!(e).into()),
489            #[cfg(all(not(feature = "eyre"), feature = "anyhow"))]
490            _ => Self::UnknownError(anyhow::anyhow!(e).into()),
491            #[cfg(all(not(feature = "eyre"), not(feature = "anyhow")))]
492            _ => {
493                let e: Box<dyn std::error::Error + Sync + Send> = format!("{e:?}").into();
494                Self::UnknownError(e)
495            }
496        }
497    }
498}
499
500impl From<ReqwestError> for Error {
501    fn from(e: ReqwestError) -> Self {
502        Error::Http(HttpError::Reqwest(e))
503    }
504}
505
506/// Errors that can happen when interacting with the beacon API.
507#[derive(Debug, Error)]
508pub enum BeaconError {
509    // A network error occurred.
510    #[error("Network error: {0}")]
511    Network(#[from] HttpError),
512
513    // The beacon information is not found.
514    #[error("Existing beacon information not found.")]
515    NotFound,
516
517    // The redacted event is not an error, but it's not useful for the client.
518    #[error("Beacon event is redacted and cannot be processed.")]
519    Redacted,
520
521    // The client must join the room to access the beacon information.
522    #[error("Must join the room to access beacon information.")]
523    Stripped,
524
525    // The beacon event could not be deserialized.
526    #[error("Deserialization error: {0}")]
527    Deserialization(#[from] serde_json::Error),
528
529    // The beacon event is expired.
530    #[error("The beacon event has expired.")]
531    NotLive,
532
533    // Allow for other errors to be wrapped.
534    #[error("Other error: {0}")]
535    Other(Box<Error>),
536}
537
538impl From<Error> for BeaconError {
539    fn from(err: Error) -> Self {
540        BeaconError::Other(Box::new(err))
541    }
542}
543
544/// Errors that can happen when refreshing an access token.
545///
546/// This is usually only returned by [`Client::refresh_access_token()`], unless
547/// [handling refresh tokens] is activated for the `Client`.
548///
549/// [`Client::refresh_access_token()`]: crate::Client::refresh_access_token()
550/// [handling refresh tokens]: crate::ClientBuilder::handle_refresh_tokens()
551#[derive(Debug, Error, Clone)]
552pub enum RefreshTokenError {
553    /// Tried to send a refresh token request without a refresh token.
554    #[error("missing refresh token")]
555    RefreshTokenRequired,
556
557    /// An error occurred interacting with the native Matrix authentication API.
558    #[error(transparent)]
559    MatrixAuth(Arc<HttpError>),
560
561    /// An error occurred interacting with the OpenID Connect API.
562    #[cfg(feature = "experimental-oidc")]
563    #[error(transparent)]
564    Oidc(#[from] Arc<crate::authentication::oidc::OidcError>),
565}
566
567/// Errors that can occur when manipulating push notification settings.
568#[derive(Debug, Error, Clone, PartialEq)]
569pub enum NotificationSettingsError {
570    /// Invalid parameter.
571    #[error("Invalid parameter `{0}`")]
572    InvalidParameter(String),
573    /// Unable to add push rule.
574    #[error("Unable to add push rule")]
575    UnableToAddPushRule,
576    /// Unable to remove push rule.
577    #[error("Unable to remove push rule")]
578    UnableToRemovePushRule,
579    /// Unable to update push rule.
580    #[error("Unable to update push rule")]
581    UnableToUpdatePushRule,
582    /// Rule not found
583    #[error("Rule `{0}` not found")]
584    RuleNotFound(String),
585    /// Unable to save the push rules
586    #[error("Unable to save push rules")]
587    UnableToSavePushRules,
588}
589
590impl From<InsertPushRuleError> for NotificationSettingsError {
591    fn from(_: InsertPushRuleError) -> Self {
592        Self::UnableToAddPushRule
593    }
594}
595
596impl From<RemovePushRuleError> for NotificationSettingsError {
597    fn from(_: RemovePushRuleError) -> Self {
598        Self::UnableToRemovePushRule
599    }
600}
601
602#[derive(Debug, Error)]
603#[error("expected: {expected}, got: {got:?}")]
604pub struct WrongRoomState {
605    expected: &'static str,
606    got: RoomState,
607}
608
609impl WrongRoomState {
610    pub(crate) fn new(expected: &'static str, got: RoomState) -> Self {
611        Self { expected, got }
612    }
613}