Skip to main content

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    BootstrapCrossSigningError, CryptoStoreError, DecryptorError, KeyExportError, MegolmError,
26    OlmError, SignatureError,
27};
28use matrix_sdk_base::{
29    Error as SdkBaseError, QueueWedgeError, RoomState, StoreError,
30    cross_process_lock::CrossProcessLockUnobtained, event_cache::store::EventCacheStoreError,
31    media::store::MediaStoreError,
32};
33use oauth2::{RequestTokenError, basic::BasicErrorResponseType};
34use reqwest::Error as ReqwestError;
35use ruma::{
36    IdParseError,
37    api::{
38        client::uiaa::{UiaaInfo, UiaaResponse},
39        error::{ErrorKind, FromHttpResponseError, IntoHttpError, RetryAfter},
40    },
41    events::{room::power_levels::PowerLevelsError, tag::InvalidUserTagName},
42    push::{InsertPushRuleError, RemovePushRuleError},
43    time::SystemTime,
44};
45use serde_json::Error as JsonError;
46use thiserror::Error;
47use url::ParseError as UrlParseError;
48
49use crate::{
50    authentication::oauth::OAuthError, cross_process_lock::CrossProcessLockError,
51    event_cache::EventCacheError, media::MediaError, room::reply::ReplyError,
52    sliding_sync::Error as SlidingSyncError,
53};
54
55/// Result type of the matrix-sdk.
56pub type Result<T, E = Error> = std::result::Result<T, E>;
57
58/// Result type of a pure HTTP request.
59pub type HttpResult<T> = std::result::Result<T, HttpError>;
60
61/// An error response from a Matrix API call, using a client API specific
62/// representation if the endpoint is from that.
63pub type RumaApiError = UiaaResponse;
64
65/// An HTTP error, representing either a connection error or an error while
66/// converting the raw HTTP response into a Matrix response.
67#[derive(Error, Debug)]
68pub enum HttpError {
69    /// Error at the HTTP layer.
70    #[error(transparent)]
71    Reqwest(#[from] ReqwestError),
72
73    /// API response error (deserialization, or a Matrix-specific error).
74    // `Box` its inner value to reduce the enum size.
75    #[error(transparent)]
76    Api(#[from] Box<FromHttpResponseError<RumaApiError>>),
77
78    /// Error when creating an API request (e.g. serialization of
79    /// body/headers/query parameters).
80    #[error(transparent)]
81    IntoHttp(IntoHttpError),
82
83    /// Error while refreshing the access token.
84    #[error(transparent)]
85    RefreshToken(RefreshTokenError),
86
87    /// Error while fetching data that is cached.
88    ///
89    /// This variant is present for convenience because cached data wraps
90    /// [`HttpError`] into an [`Arc`] to be able to clone it.
91    #[error(transparent)]
92    Cached(Arc<HttpError>),
93}
94
95#[rustfmt::skip] // stop rustfmt breaking the `<code>` in docs across multiple lines
96impl HttpError {
97    /// If `self` is
98    /// <code>[Api](Self::Api)([Server](FromHttpResponseError::Server)(e))</code>,
99    /// returns `Some(e)`.
100    ///
101    /// Otherwise, returns `None`.
102    pub fn as_ruma_api_error(&self) -> Option<&RumaApiError> {
103        match self {
104            Self::Api(error) => {
105                as_variant!(error.as_ref(), FromHttpResponseError::Server)
106            },
107            _ => None
108        }
109    }
110}
111
112// Another impl block that's formatted with rustfmt.
113impl HttpError {
114    /// If `self` is a server error in the `errcode` + `error` format expected
115    /// for client API endpoints, returns it.
116    pub fn as_client_api_error(&self) -> Option<&ruma::api::error::Error> {
117        self.as_ruma_api_error().and_then(as_variant!(UiaaResponse::MatrixError))
118    }
119
120    /// If `self` is a server error in the `errcode` + `error` format expected
121    /// for client API endpoints, returns the error kind (`errcode`).
122    pub fn client_api_error_kind(&self) -> Option<&ErrorKind> {
123        self.as_client_api_error().and_then(ruma::api::error::Error::error_kind)
124    }
125
126    /// Whether the homeserver rejected the access token this request was sent
127    /// with.
128    pub(crate) fn is_rejected_token(&self) -> bool {
129        match self {
130            Self::RefreshToken(_) => true,
131            Self::Cached(inner) => inner.is_rejected_token(),
132            _ => matches!(self.client_api_error_kind(), Some(ErrorKind::UnknownToken(_))),
133        }
134    }
135
136    /// Try to destructure the error into a user-interactive auth info.
137    ///
138    /// Some requests require user-interactive auth, doing such a request will
139    /// always fail the first time with a 401 status code, the response body
140    /// will contain info on how the client can authenticate.
141    ///
142    /// The request will need to be retried, this time containing additional
143    /// authentication data.
144    ///
145    /// This method is a convenience method to get to the info the server
146    /// returned on the first, failed request.
147    pub fn as_uiaa_response(&self) -> Option<&UiaaInfo> {
148        self.as_ruma_api_error().and_then(as_variant!(UiaaResponse::AuthResponse))
149    }
150
151    /// Returns whether an HTTP error response should be qualified as transient
152    /// or permanent.
153    pub(crate) fn retry_kind(&self) -> RetryKind {
154        match self {
155            // If it was a plain network error, it's either that we're
156            // disconnected from the internet, or that the remote is, so retry a
157            // few times.
158            HttpError::Reqwest(_) => RetryKind::NetworkFailure,
159
160            HttpError::Api(error) => match error.as_ref() {
161                FromHttpResponseError::Server(api_error) => RetryKind::from_api_error(api_error),
162                _ => RetryKind::Permanent,
163            },
164
165            HttpError::RefreshToken(error) => {
166                if error.is_session_rejected() {
167                    RetryKind::Permanent
168                } else {
169                    RetryKind::Transient { retry_after: None }
170                }
171            }
172
173            _ => RetryKind::Permanent,
174        }
175    }
176
177    /// If this is a server error, returns whether it matches the expected
178    /// format for an endpoint that is not implemented.
179    pub fn is_endpoint_not_implemented(&self) -> bool {
180        self.as_client_api_error().is_some_and(|error| error.is_endpoint_not_implemented())
181    }
182}
183
184impl From<FromHttpResponseError<RumaApiError>> for HttpError {
185    fn from(value: FromHttpResponseError<RumaApiError>) -> Self {
186        Self::Api(Box::new(value))
187    }
188}
189
190/// The HTTP status code that some reverse proxies return for server errors.
191const STATUS_CODE_PROXY_UNKNOWN_SERVER_ERROR: u16 = 520;
192
193/// How should we behave with respect to retry behavior after an [`HttpError`]
194/// happened?
195#[derive(Debug)]
196pub(crate) enum RetryKind {
197    /// The request failed because of an error at the network layer.
198    NetworkFailure,
199
200    /// The request failed with a "transient" error, meaning it could be retried
201    /// either soon, or after a given amount of time expressed in `retry_after`.
202    Transient {
203        // This is used only for attempts to retry, so on non-wasm32 code (in the `native` module).
204        #[cfg_attr(target_family = "wasm", allow(dead_code))]
205        retry_after: Option<Duration>,
206    },
207
208    /// The request failed with a non-transient error, and retrying it would
209    /// likely cause the same error again, so it's not worth retrying.
210    Permanent,
211}
212
213impl RetryKind {
214    /// Construct a [`RetryKind`] from a Ruma API error.
215    ///
216    /// The Ruma API error is for errors which have the standard error response
217    /// format defined in the [spec].
218    ///
219    /// [spec]: https://spec.matrix.org/v1.11/client-server-api/#standard-error-response
220    fn from_api_error(api_error: &UiaaResponse) -> Self {
221        match api_error {
222            UiaaResponse::MatrixError(client_error) => match client_error.error_kind() {
223                Some(ErrorKind::LimitExceeded(limit_exceeded)) => {
224                    RetryKind::from_retry_after(limit_exceeded.retry_after.as_ref())
225                }
226                Some(ErrorKind::Unrecognized) => RetryKind::Permanent,
227                _ => RetryKind::from_status_code(client_error.status_code)
228                    // This should only happen for non-error status codes, which shouldn't be
229                    // checked here
230                    .unwrap_or(RetryKind::Permanent),
231            },
232            UiaaResponse::AuthResponse(_) => RetryKind::Permanent,
233        }
234    }
235
236    /// Create a [`RetryKind`] if we have found a [`RetryAfter`] defined in an
237    /// error.
238    ///
239    /// This method should be used for errors where the server explicitly tells
240    /// us how long we must wait before we retry the request again.
241    fn from_retry_after(retry_after: Option<&RetryAfter>) -> Self {
242        let retry_after = retry_after.and_then(|retry_after| match retry_after {
243            RetryAfter::Delay(d) => Some(*d),
244            // `duration_since` fails if the date is already in the past, in which case we
245            // fall back to the regular backoff.
246            RetryAfter::DateTime(time) => time.duration_since(SystemTime::now()).ok(),
247        });
248
249        Self::Transient { retry_after }
250    }
251
252    /// Construct a [`RetryKind`] from a HTTP [`StatusCode`].
253    ///
254    /// This should be used if we don't have a more specific Matrix style error
255    /// which gives us more information about the nature of the error, i.e. if
256    /// we received an error from a reverse proxy while the Matrix homeserver is
257    /// down.
258    ///
259    /// Non-error status codes will return a [None] value.
260    fn from_status_code(status_code: StatusCode) -> Option<Self> {
261        // The list of status code below should be considered retryable, since
262        // they reference an issue that should fix itself with time. Any
263        // other status codes besides 520 (unknown server error emitted by
264        // reverse proxies) should be considered permanent.
265        match status_code {
266            // The timeout should probably have been processed somewhere else as a different error
267            // type, but we can add it for completeness
268            StatusCode::REQUEST_TIMEOUT
269            | StatusCode::TOO_MANY_REQUESTS
270            | StatusCode::INTERNAL_SERVER_ERROR
271            | StatusCode::BAD_GATEWAY
272            | StatusCode::SERVICE_UNAVAILABLE
273            | StatusCode::GATEWAY_TIMEOUT
274            | StatusCode::INSUFFICIENT_STORAGE => Some(RetryKind::Transient { retry_after: None }),
275            _ => {
276                // Special case: unknown server error from a reverse proxy, the
277                // server might recover
278                if status_code.as_u16() == STATUS_CODE_PROXY_UNKNOWN_SERVER_ERROR {
279                    Some(RetryKind::Transient { retry_after: None })
280                } else if status_code.is_client_error() || status_code.is_server_error() {
281                    Some(RetryKind::Permanent)
282                } else {
283                    None
284                }
285            }
286        }
287    }
288}
289
290/// Internal representation of errors.
291#[derive(Error, Debug)]
292#[non_exhaustive]
293pub enum Error {
294    /// Error doing an HTTP request.
295    #[error(transparent)]
296    Http(Box<HttpError>),
297
298    /// Queried endpoint requires authentication but was called on an anonymous
299    /// client.
300    #[error("the queried endpoint requires authentication but was called before logging in")]
301    AuthenticationRequired,
302
303    /// This request failed because the local data wasn't sufficient.
304    #[error("Local cache doesn't contain all necessary data to perform the action.")]
305    InsufficientData,
306
307    /// Attempting to restore a session after the olm-machine has already been
308    /// set up fails
309    #[cfg(feature = "e2e-encryption")]
310    #[error("The olm machine has already been initialized")]
311    BadCryptoStoreState,
312
313    /// Attempting to access the olm-machine but it is not yet available.
314    #[cfg(feature = "e2e-encryption")]
315    #[error("The olm machine isn't yet available")]
316    NoOlmMachine,
317
318    /// An error de/serializing type for the `StateStore`
319    #[error(transparent)]
320    SerdeJson(#[from] JsonError),
321
322    /// An IO error happened.
323    #[error(transparent)]
324    Io(#[from] IoError),
325
326    /// An error occurred in the crypto store.
327    #[cfg(feature = "e2e-encryption")]
328    #[error(transparent)]
329    CryptoStoreError(Box<CryptoStoreError>),
330
331    /// An error occurred with a cross-process store lock.
332    #[error(transparent)]
333    CrossProcessLockError(Box<CrossProcessLockError>),
334
335    /// An error occurred during a E2EE operation.
336    #[cfg(feature = "e2e-encryption")]
337    #[error(transparent)]
338    OlmError(Box<OlmError>),
339
340    /// An error occurred during a E2EE group operation.
341    #[cfg(feature = "e2e-encryption")]
342    #[error(transparent)]
343    MegolmError(Box<MegolmError>),
344
345    /// An error occurred during decryption.
346    #[cfg(feature = "e2e-encryption")]
347    #[error(transparent)]
348    DecryptorError(#[from] DecryptorError),
349
350    /// An error occurred during signing or verifying.
351    #[cfg(feature = "e2e-encryption")]
352    #[error(transparent)]
353    SignatureError(#[from] SignatureError),
354
355    /// An error occurred in the state store.
356    #[error(transparent)]
357    StateStore(Box<StoreError>),
358
359    /// An error occurred in the event cache store.
360    #[error(transparent)]
361    EventCacheStore(Box<EventCacheStoreError>),
362
363    /// An error occurred in the media store.
364    #[error(transparent)]
365    MediaStore(Box<MediaStoreError>),
366
367    /// An error encountered when trying to parse an identifier.
368    #[error(transparent)]
369    Identifier(#[from] IdParseError),
370
371    /// An error encountered when trying to parse a url.
372    #[error(transparent)]
373    Url(#[from] UrlParseError),
374
375    /// An error while scanning a QR code.
376    #[cfg(feature = "qrcode")]
377    #[error(transparent)]
378    QrCodeScanError(Box<ScanError>),
379
380    /// An error encountered when trying to parse a user tag name.
381    #[error(transparent)]
382    UserTagName(#[from] InvalidUserTagName),
383
384    /// An error occurred within sliding-sync
385    #[error(transparent)]
386    SlidingSync(Box<SlidingSyncError>),
387
388    /// Attempted to call a method on a room that requires the user to have a
389    /// specific membership state in the room, but the membership state is
390    /// different.
391    #[error("wrong room state: {0}")]
392    WrongRoomState(Box<WrongRoomState>),
393
394    /// Session callbacks have been set multiple times.
395    #[error("session callbacks have been set multiple times")]
396    MultipleSessionCallbacks,
397
398    /// An error occurred interacting with the OAuth 2.0 API.
399    #[error(transparent)]
400    OAuth(Box<OAuthError>),
401
402    /// A concurrent request to a deduplicated request has failed.
403    #[error("a concurrent request failed; see logs for details")]
404    ConcurrentRequestFailed,
405
406    /// An other error was raised.
407    ///
408    /// This might happen because encryption was enabled on the base-crate but
409    /// not here and that raised.
410    #[cfg(not(target_family = "wasm"))]
411    #[error("unknown error: {0}")]
412    UnknownError(Box<dyn std::error::Error + Send + Sync>),
413
414    /// An other error was raised.
415    #[cfg(target_family = "wasm")]
416    #[error("unknown error: {0}")]
417    UnknownError(Box<dyn std::error::Error>),
418
419    /// An error coming from the event cache subsystem.
420    #[error(transparent)]
421    EventCache(Box<EventCacheError>),
422
423    /// An item has been wedged in the send queue.
424    #[error(transparent)]
425    SendQueueWedgeError(Box<QueueWedgeError>),
426
427    /// Backups are not enabled
428    #[error("backups are not enabled")]
429    BackupNotEnabled,
430
431    /// It's forbidden to ignore your own user.
432    #[error("can't ignore the logged-in user")]
433    CantIgnoreLoggedInUser,
434
435    /// An error happened during handling of a media subrequest.
436    #[error(transparent)]
437    Media(#[from] MediaError),
438
439    /// An error happened while attempting to reply to an event.
440    #[error(transparent)]
441    ReplyError(#[from] ReplyError),
442
443    /// An error happened while attempting to change power levels.
444    #[error("power levels error: {0}")]
445    PowerLevels(#[from] PowerLevelsError),
446
447    /// We timed out attempting to complete an operation.
448    #[error("timed out")]
449    Timeout,
450}
451
452#[rustfmt::skip] // stop rustfmt breaking the `<code>` in docs across multiple lines
453impl Error {
454    /// If `self` is
455    /// <code>[Http](Self::Http)([Api](HttpError::Api)([Server](FromHttpResponseError::Server)(e)))</code>,
456    /// returns `Some(e)`.
457    ///
458    /// Otherwise, returns `None`.
459    pub fn as_ruma_api_error(&self) -> Option<&RumaApiError> {
460        as_variant!(self, Self::Http).and_then(|e| e.as_ruma_api_error())
461    }
462
463    /// If `self` is a server error in the `errcode` + `error` format expected
464    /// for client API endpoints, returns it.
465    pub fn as_client_api_error(&self) -> Option<&ruma::api::error::Error> {
466        self.as_ruma_api_error().and_then(as_variant!(UiaaResponse::MatrixError))
467    }
468
469    /// If `self` is a server error in the `errcode` + `error` format expected
470    /// for client API endpoints, returns the error kind (`errcode`).
471    pub fn client_api_error_kind(&self) -> Option<&ErrorKind> {
472        self.as_client_api_error().and_then(ruma::api::error::Error::error_kind)
473    }
474
475    /// Try to destructure the error into an universal interactive auth info.
476    ///
477    /// Some requests require universal interactive auth, doing such a request
478    /// will always fail the first time with a 401 status code, the response
479    /// body will contain info how the client can authenticate.
480    ///
481    /// The request will need to be retried, this time containing additional
482    /// authentication data.
483    ///
484    /// This method is an convenience method to get to the info the server
485    /// returned on the first, failed request.
486    pub fn as_uiaa_response(&self) -> Option<&UiaaInfo> {
487        self.as_ruma_api_error().and_then(as_variant!(UiaaResponse::AuthResponse))
488    }
489}
490
491impl From<HttpError> for Error {
492    fn from(error: HttpError) -> Self {
493        Error::Http(Box::new(error))
494    }
495}
496
497#[cfg(feature = "e2e-encryption")]
498impl From<CryptoStoreError> for Error {
499    fn from(error: CryptoStoreError) -> Self {
500        Error::CryptoStoreError(Box::new(error))
501    }
502}
503
504impl From<CrossProcessLockError> for Error {
505    fn from(error: CrossProcessLockError) -> Self {
506        Error::CrossProcessLockError(Box::new(error))
507    }
508}
509
510impl From<CrossProcessLockUnobtained> for Error {
511    fn from(error: CrossProcessLockUnobtained) -> Self {
512        CrossProcessLockError::from(error).into()
513    }
514}
515
516#[cfg(feature = "e2e-encryption")]
517impl From<OlmError> for Error {
518    fn from(error: OlmError) -> Self {
519        Error::OlmError(Box::new(error))
520    }
521}
522
523#[cfg(feature = "e2e-encryption")]
524impl From<MegolmError> for Error {
525    fn from(error: MegolmError) -> Self {
526        Error::MegolmError(Box::new(error))
527    }
528}
529
530impl From<StoreError> for Error {
531    fn from(error: StoreError) -> Self {
532        Error::StateStore(Box::new(error))
533    }
534}
535
536impl From<EventCacheStoreError> for Error {
537    fn from(error: EventCacheStoreError) -> Self {
538        Error::EventCacheStore(Box::new(error))
539    }
540}
541
542impl From<MediaStoreError> for Error {
543    fn from(error: MediaStoreError) -> Self {
544        Error::MediaStore(Box::new(error))
545    }
546}
547
548#[cfg(feature = "qrcode")]
549impl From<ScanError> for Error {
550    fn from(error: ScanError) -> Self {
551        Error::QrCodeScanError(Box::new(error))
552    }
553}
554
555impl From<SlidingSyncError> for Error {
556    fn from(error: SlidingSyncError) -> Self {
557        Error::SlidingSync(Box::new(error))
558    }
559}
560
561impl From<OAuthError> for Error {
562    fn from(error: OAuthError) -> Self {
563        Error::OAuth(Box::new(error))
564    }
565}
566
567impl From<EventCacheError> for Error {
568    fn from(error: EventCacheError) -> Self {
569        Error::EventCache(Box::new(error))
570    }
571}
572
573impl From<QueueWedgeError> for Error {
574    fn from(error: QueueWedgeError) -> Self {
575        Error::SendQueueWedgeError(Box::new(error))
576    }
577}
578
579#[cfg(feature = "e2e-encryption")]
580impl From<BootstrapCrossSigningError> for Error {
581    fn from(error: BootstrapCrossSigningError) -> Self {
582        match error {
583            BootstrapCrossSigningError::CryptoStore(e) => e.into(),
584            BootstrapCrossSigningError::Signature(e) => e.into(),
585        }
586    }
587}
588
589/// Error for the room key importing functionality.
590#[cfg(feature = "e2e-encryption")]
591#[derive(Error, Debug)]
592// This is allowed because key importing isn't enabled under wasm.
593#[allow(dead_code)]
594pub enum RoomKeyImportError {
595    /// An error de/serializing type for the `StateStore`
596    #[error(transparent)]
597    SerdeJson(#[from] JsonError),
598
599    /// The crypto store isn't yet open. Logging in is required to open the
600    /// crypto store.
601    #[error("The crypto store hasn't been yet opened, can't import yet.")]
602    StoreClosed,
603
604    /// An IO error happened.
605    #[error(transparent)]
606    Io(#[from] IoError),
607
608    /// An error occurred in the crypto store.
609    #[error(transparent)]
610    CryptoStore(#[from] CryptoStoreError),
611
612    /// An error occurred while importing the key export.
613    #[error(transparent)]
614    Export(#[from] KeyExportError),
615}
616
617impl From<FromHttpResponseError<ruma::api::error::Error>> for HttpError {
618    fn from(err: FromHttpResponseError<ruma::api::error::Error>) -> Self {
619        Self::Api(Box::new(err.map(Into::into)))
620    }
621}
622
623impl From<SdkBaseError> for Error {
624    fn from(e: SdkBaseError) -> Self {
625        match e {
626            SdkBaseError::StateStore(e) => Self::StateStore(Box::new(e)),
627            #[cfg(feature = "e2e-encryption")]
628            SdkBaseError::CryptoStore(e) => Self::CryptoStoreError(Box::new(e)),
629            #[cfg(feature = "e2e-encryption")]
630            SdkBaseError::BadCryptoStoreState => Self::BadCryptoStoreState,
631            #[cfg(feature = "e2e-encryption")]
632            SdkBaseError::OlmError(e) => Self::OlmError(Box::new(e)),
633            #[cfg(feature = "eyre")]
634            _ => Self::UnknownError(eyre::eyre!(e).into()),
635            #[cfg(all(not(feature = "eyre"), feature = "anyhow", not(target_family = "wasm")))]
636            _ => Self::UnknownError(anyhow::anyhow!(e).into()),
637            #[cfg(all(not(feature = "eyre"), feature = "anyhow", target_family = "wasm"))]
638            _ => Self::UnknownError(e.into()),
639            #[cfg(all(
640                not(feature = "eyre"),
641                not(feature = "anyhow"),
642                not(target_family = "wasm")
643            ))]
644            _ => {
645                let e: Box<dyn std::error::Error + Send + Sync> = format!("{e:?}").into();
646                Self::UnknownError(e)
647            }
648            #[cfg(all(not(feature = "eyre"), not(feature = "anyhow"), target_family = "wasm"))]
649            _ => {
650                let e: Box<dyn std::error::Error> = format!("{e:?}").into();
651                Self::UnknownError(e)
652            }
653        }
654    }
655}
656
657impl From<ReqwestError> for Error {
658    fn from(e: ReqwestError) -> Self {
659        Error::Http(Box::new(HttpError::Reqwest(e)))
660    }
661}
662
663/// Errors that can happen when interacting with the beacon API.
664#[derive(Debug, Error)]
665pub enum BeaconError {
666    /// A network error occurred.
667    #[error("Network error: {0}")]
668    Network(#[from] HttpError),
669
670    /// The beacon information is not found.
671    #[error("Existing beacon information not found.")]
672    NotFound,
673
674    /// The redacted event is not an error, but it's not useful for the client.
675    #[error("Beacon event is redacted and cannot be processed.")]
676    Redacted,
677
678    /// The client must join the room to access the beacon information.
679    #[error("Must join the room to access beacon information.")]
680    Stripped,
681
682    /// The beacon event could not be deserialized.
683    #[error("Deserialization error: {0}")]
684    Deserialization(#[from] serde_json::Error),
685
686    /// The beacon event is expired.
687    #[error("The beacon event has expired.")]
688    NotLive,
689
690    /// Allow for other errors to be wrapped.
691    #[error("Other error: {0}")]
692    Other(Box<Error>),
693}
694
695impl From<Error> for BeaconError {
696    fn from(err: Error) -> Self {
697        BeaconError::Other(Box::new(err))
698    }
699}
700
701/// Errors that can happen when refreshing an access token.
702///
703/// This is usually only returned by [`Client::refresh_access_token()`], unless
704/// [handling refresh tokens] is activated for the `Client`.
705///
706/// [`Client::refresh_access_token()`]: crate::Client::refresh_access_token()
707/// [handling refresh tokens]: crate::ClientBuilder::handle_refresh_tokens()
708#[derive(Debug, Error, Clone)]
709pub enum RefreshTokenError {
710    /// Tried to send a refresh token request without a refresh token.
711    #[error("missing refresh token")]
712    RefreshTokenRequired,
713
714    /// An error occurred interacting with the native Matrix authentication API.
715    #[error(transparent)]
716    MatrixAuth(Arc<HttpError>),
717
718    /// An error occurred interacting with the OAuth 2.0 API.
719    #[error(transparent)]
720    OAuth(#[from] Arc<OAuthError>),
721}
722
723impl RefreshTokenError {
724    /// Whether the session was rejected, as opposed to the refresh request
725    /// itself not getting through.
726    pub(crate) fn is_session_rejected(&self) -> bool {
727        match self {
728            // There is no refresh token to exchange and retrying won't fix it.
729            Self::RefreshTokenRequired => true,
730
731            Self::MatrixAuth(http_error) => http_error
732                .as_client_api_error()
733                .is_some_and(|error| !error.status_code.is_server_error()),
734
735            Self::OAuth(oauth_error) => matches!(
736                &**oauth_error,
737                OAuthError::RefreshToken(RequestTokenError::ServerResponse(error_response))
738                    if *error_response.error() == BasicErrorResponseType::InvalidGrant
739            ),
740        }
741    }
742}
743
744/// Errors that can occur when manipulating push notification settings.
745#[derive(Debug, Error, Clone, PartialEq)]
746pub enum NotificationSettingsError {
747    /// Invalid parameter.
748    #[error("Invalid parameter `{0}`")]
749    InvalidParameter(String),
750    /// Unable to add push rule.
751    #[error("Unable to add push rule")]
752    UnableToAddPushRule,
753    /// Unable to remove push rule.
754    #[error("Unable to remove push rule")]
755    UnableToRemovePushRule,
756    /// Unable to update push rule.
757    #[error("Unable to update push rule")]
758    UnableToUpdatePushRule,
759    /// Rule not found
760    #[error("Rule `{0}` not found")]
761    RuleNotFound(String),
762    /// Unable to save the push rules
763    #[error("Unable to save push rules")]
764    UnableToSavePushRules,
765}
766
767impl NotificationSettingsError {
768    /// Whether this error is the [`RuleNotFound`](Self::RuleNotFound) variant.
769    pub fn is_rule_not_found(&self) -> bool {
770        matches!(self, Self::RuleNotFound(_))
771    }
772}
773
774impl From<InsertPushRuleError> for NotificationSettingsError {
775    fn from(_: InsertPushRuleError) -> Self {
776        Self::UnableToAddPushRule
777    }
778}
779
780impl From<RemovePushRuleError> for NotificationSettingsError {
781    fn from(_: RemovePushRuleError) -> Self {
782        Self::UnableToRemovePushRule
783    }
784}
785
786#[derive(Debug, Error)]
787#[error("expected: {expected}, got: {got:?}")]
788pub struct WrongRoomState {
789    expected: &'static str,
790    got: RoomState,
791}
792
793impl WrongRoomState {
794    pub(crate) fn new(expected: &'static str, got: RoomState) -> Self {
795        Self { expected, got }
796    }
797}
798
799#[cfg(test)]
800mod tests {
801    use std::time::Duration;
802
803    use assert_matches::assert_matches;
804    use http::StatusCode;
805    use ruma::{
806        api::{EndpointError as _, error::RetryAfter},
807        time::SystemTime,
808    };
809
810    use super::{
811        HttpError, RefreshTokenError, RetryKind, RumaApiError,
812        STATUS_CODE_PROXY_UNKNOWN_SERVER_ERROR,
813    };
814    use crate::error::FromHttpResponseError;
815
816    /// The error a request gets back when refreshing its access token failed
817    /// against the homeserver with the given status code and body.
818    fn refresh_failure(status_code: StatusCode, body: &str) -> HttpError {
819        let response = http::Response::builder().status(status_code).body(body.as_bytes()).unwrap();
820        let api_error = RumaApiError::from_http_response(response);
821
822        HttpError::RefreshToken(RefreshTokenError::MatrixAuth(std::sync::Arc::new(HttpError::Api(
823            Box::new(FromHttpResponseError::Server(api_error)),
824        ))))
825    }
826
827    #[test]
828    fn test_retry_kind_of_a_failed_refresh() {
829        // A 5xx should be retried worth sending again.
830        assert_matches!(
831            refresh_failure(StatusCode::BAD_GATEWAY, "").retry_kind(),
832            RetryKind::Transient { .. }
833        );
834
835        // The homeserver rejected the refresh token so retrying would only fail
836        // again.
837        assert_matches!(
838            refresh_failure(
839                StatusCode::UNAUTHORIZED,
840                r#"{"errcode": "M_UNKNOWN_TOKEN", "error": "Token is not active"}"#,
841            )
842            .retry_kind(),
843            RetryKind::Permanent
844        );
845
846        // There is no refresh token to exchange and retrying won't work.
847        assert_matches!(
848            HttpError::RefreshToken(RefreshTokenError::RefreshTokenRequired).retry_kind(),
849            RetryKind::Permanent
850        );
851    }
852
853    #[test]
854    fn test_retry_kind_from_retry_after() {
855        assert_matches!(
856            RetryKind::from_retry_after(None),
857            RetryKind::Transient { retry_after: None }
858        );
859
860        assert_matches!(
861            RetryKind::from_retry_after(Some(&RetryAfter::Delay(Duration::from_secs(5)))),
862            RetryKind::Transient { retry_after: Some(delay) } => {
863                assert_eq!(delay, Duration::from_secs(5));
864            }
865        );
866
867        // A date in the future is turned into the delay until that date.
868        let in_a_minute = SystemTime::now() + Duration::from_secs(60);
869        assert_matches!(
870            RetryKind::from_retry_after(Some(&RetryAfter::DateTime(in_a_minute))),
871            RetryKind::Transient { retry_after: Some(delay) } => {
872                assert!(delay > Duration::from_secs(55));
873                assert!(delay <= Duration::from_secs(60));
874            }
875        );
876
877        // A date in the past falls back to the regular backoff.
878        let a_minute_ago = SystemTime::now() - Duration::from_secs(60);
879        assert_matches!(
880            RetryKind::from_retry_after(Some(&RetryAfter::DateTime(a_minute_ago))),
881            RetryKind::Transient { retry_after: None }
882        );
883    }
884
885    #[test]
886    fn test_retry_kind_of_http_status_codes() {
887        let retry_status_code = [
888            StatusCode::REQUEST_TIMEOUT,
889            StatusCode::TOO_MANY_REQUESTS,
890            StatusCode::INTERNAL_SERVER_ERROR,
891            StatusCode::BAD_GATEWAY,
892            StatusCode::SERVICE_UNAVAILABLE,
893            StatusCode::GATEWAY_TIMEOUT,
894            StatusCode::INSUFFICIENT_STORAGE,
895            StatusCode::from_u16(STATUS_CODE_PROXY_UNKNOWN_SERVER_ERROR).unwrap(),
896        ];
897
898        // Check retryable status codes
899        for code in retry_status_code.iter() {
900            assert_matches!(
901                RetryKind::from_status_code(code.to_owned()),
902                Some(RetryKind::Transient { .. })
903            );
904        }
905
906        // Check any possible (existing) non-error code
907        for i in 1..400 {
908            let Ok(code) = StatusCode::from_u16(i) else {
909                continue;
910            };
911            if retry_status_code.contains(&code) {
912                continue;
913            }
914            assert_matches!(RetryKind::from_status_code(code.to_owned()), None);
915        }
916
917        // Check any possible (existing) error code
918        for i in 400..600 {
919            let Ok(code) = StatusCode::from_u16(i) else {
920                continue;
921            };
922            if retry_status_code.contains(&code) {
923                continue;
924            }
925            assert_matches!(RetryKind::from_status_code(code), Some(RetryKind::Permanent));
926        }
927    }
928}