1use 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
55pub type Result<T, E = Error> = std::result::Result<T, E>;
57
58pub type HttpResult<T> = std::result::Result<T, HttpError>;
60
61pub type RumaApiError = UiaaResponse;
64
65#[derive(Error, Debug)]
68pub enum HttpError {
69 #[error(transparent)]
71 Reqwest(#[from] ReqwestError),
72
73 #[error(transparent)]
76 Api(#[from] Box<FromHttpResponseError<RumaApiError>>),
77
78 #[error(transparent)]
81 IntoHttp(IntoHttpError),
82
83 #[error(transparent)]
85 RefreshToken(RefreshTokenError),
86
87 #[error(transparent)]
92 Cached(Arc<HttpError>),
93}
94
95#[rustfmt::skip] impl HttpError {
97 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
112impl HttpError {
114 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 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 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 pub fn as_uiaa_response(&self) -> Option<&UiaaInfo> {
148 self.as_ruma_api_error().and_then(as_variant!(UiaaResponse::AuthResponse))
149 }
150
151 pub(crate) fn retry_kind(&self) -> RetryKind {
154 match self {
155 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 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
190const STATUS_CODE_PROXY_UNKNOWN_SERVER_ERROR: u16 = 520;
192
193#[derive(Debug)]
196pub(crate) enum RetryKind {
197 NetworkFailure,
199
200 Transient {
203 #[cfg_attr(target_family = "wasm", allow(dead_code))]
205 retry_after: Option<Duration>,
206 },
207
208 Permanent,
211}
212
213impl RetryKind {
214 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 .unwrap_or(RetryKind::Permanent),
231 },
232 UiaaResponse::AuthResponse(_) => RetryKind::Permanent,
233 }
234 }
235
236 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 RetryAfter::DateTime(time) => time.duration_since(SystemTime::now()).ok(),
247 });
248
249 Self::Transient { retry_after }
250 }
251
252 fn from_status_code(status_code: StatusCode) -> Option<Self> {
261 match status_code {
266 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 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#[derive(Error, Debug)]
292#[non_exhaustive]
293pub enum Error {
294 #[error(transparent)]
296 Http(Box<HttpError>),
297
298 #[error("the queried endpoint requires authentication but was called before logging in")]
301 AuthenticationRequired,
302
303 #[error("Local cache doesn't contain all necessary data to perform the action.")]
305 InsufficientData,
306
307 #[cfg(feature = "e2e-encryption")]
310 #[error("The olm machine has already been initialized")]
311 BadCryptoStoreState,
312
313 #[cfg(feature = "e2e-encryption")]
315 #[error("The olm machine isn't yet available")]
316 NoOlmMachine,
317
318 #[error(transparent)]
320 SerdeJson(#[from] JsonError),
321
322 #[error(transparent)]
324 Io(#[from] IoError),
325
326 #[cfg(feature = "e2e-encryption")]
328 #[error(transparent)]
329 CryptoStoreError(Box<CryptoStoreError>),
330
331 #[error(transparent)]
333 CrossProcessLockError(Box<CrossProcessLockError>),
334
335 #[cfg(feature = "e2e-encryption")]
337 #[error(transparent)]
338 OlmError(Box<OlmError>),
339
340 #[cfg(feature = "e2e-encryption")]
342 #[error(transparent)]
343 MegolmError(Box<MegolmError>),
344
345 #[cfg(feature = "e2e-encryption")]
347 #[error(transparent)]
348 DecryptorError(#[from] DecryptorError),
349
350 #[cfg(feature = "e2e-encryption")]
352 #[error(transparent)]
353 SignatureError(#[from] SignatureError),
354
355 #[error(transparent)]
357 StateStore(Box<StoreError>),
358
359 #[error(transparent)]
361 EventCacheStore(Box<EventCacheStoreError>),
362
363 #[error(transparent)]
365 MediaStore(Box<MediaStoreError>),
366
367 #[error(transparent)]
369 Identifier(#[from] IdParseError),
370
371 #[error(transparent)]
373 Url(#[from] UrlParseError),
374
375 #[cfg(feature = "qrcode")]
377 #[error(transparent)]
378 QrCodeScanError(Box<ScanError>),
379
380 #[error(transparent)]
382 UserTagName(#[from] InvalidUserTagName),
383
384 #[error(transparent)]
386 SlidingSync(Box<SlidingSyncError>),
387
388 #[error("wrong room state: {0}")]
392 WrongRoomState(Box<WrongRoomState>),
393
394 #[error("session callbacks have been set multiple times")]
396 MultipleSessionCallbacks,
397
398 #[error(transparent)]
400 OAuth(Box<OAuthError>),
401
402 #[error("a concurrent request failed; see logs for details")]
404 ConcurrentRequestFailed,
405
406 #[cfg(not(target_family = "wasm"))]
411 #[error("unknown error: {0}")]
412 UnknownError(Box<dyn std::error::Error + Send + Sync>),
413
414 #[cfg(target_family = "wasm")]
416 #[error("unknown error: {0}")]
417 UnknownError(Box<dyn std::error::Error>),
418
419 #[error(transparent)]
421 EventCache(Box<EventCacheError>),
422
423 #[error(transparent)]
425 SendQueueWedgeError(Box<QueueWedgeError>),
426
427 #[error("backups are not enabled")]
429 BackupNotEnabled,
430
431 #[error("can't ignore the logged-in user")]
433 CantIgnoreLoggedInUser,
434
435 #[error(transparent)]
437 Media(#[from] MediaError),
438
439 #[error(transparent)]
441 ReplyError(#[from] ReplyError),
442
443 #[error("power levels error: {0}")]
445 PowerLevels(#[from] PowerLevelsError),
446
447 #[error("timed out")]
449 Timeout,
450}
451
452#[rustfmt::skip] impl Error {
454 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 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 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 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#[cfg(feature = "e2e-encryption")]
591#[derive(Error, Debug)]
592#[allow(dead_code)]
594pub enum RoomKeyImportError {
595 #[error(transparent)]
597 SerdeJson(#[from] JsonError),
598
599 #[error("The crypto store hasn't been yet opened, can't import yet.")]
602 StoreClosed,
603
604 #[error(transparent)]
606 Io(#[from] IoError),
607
608 #[error(transparent)]
610 CryptoStore(#[from] CryptoStoreError),
611
612 #[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#[derive(Debug, Error)]
665pub enum BeaconError {
666 #[error("Network error: {0}")]
668 Network(#[from] HttpError),
669
670 #[error("Existing beacon information not found.")]
672 NotFound,
673
674 #[error("Beacon event is redacted and cannot be processed.")]
676 Redacted,
677
678 #[error("Must join the room to access beacon information.")]
680 Stripped,
681
682 #[error("Deserialization error: {0}")]
684 Deserialization(#[from] serde_json::Error),
685
686 #[error("The beacon event has expired.")]
688 NotLive,
689
690 #[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#[derive(Debug, Error, Clone)]
709pub enum RefreshTokenError {
710 #[error("missing refresh token")]
712 RefreshTokenRequired,
713
714 #[error(transparent)]
716 MatrixAuth(Arc<HttpError>),
717
718 #[error(transparent)]
720 OAuth(#[from] Arc<OAuthError>),
721}
722
723impl RefreshTokenError {
724 pub(crate) fn is_session_rejected(&self) -> bool {
727 match self {
728 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#[derive(Debug, Error, Clone, PartialEq)]
746pub enum NotificationSettingsError {
747 #[error("Invalid parameter `{0}`")]
749 InvalidParameter(String),
750 #[error("Unable to add push rule")]
752 UnableToAddPushRule,
753 #[error("Unable to remove push rule")]
755 UnableToRemovePushRule,
756 #[error("Unable to update push rule")]
758 UnableToUpdatePushRule,
759 #[error("Rule `{0}` not found")]
761 RuleNotFound(String),
762 #[error("Unable to save push rules")]
764 UnableToSavePushRules,
765}
766
767impl NotificationSettingsError {
768 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 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 assert_matches!(
831 refresh_failure(StatusCode::BAD_GATEWAY, "").retry_kind(),
832 RetryKind::Transient { .. }
833 );
834
835 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 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 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 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 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 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 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}