atagen

macltok

public
Star0
Watch0

Commit 9a25b6ce66f4

tarball

atagen <boss@atagen.co>2026-10-08 13:08 UTC

4 files changed+96-5
error: carry the homeserver's errcode and message
Modified file: docs/design-decisions.md+9 additions−3 deletions
@@ -20,11 +20,17 @@ rooms, connection, encryption, verification, upload progress, session. no
2020 command results ride on it. drain it from its own task, it's bounded and the
2121 engine waits for you
2222
23-one error type for everything, `Error { kind, message, challenge }`. match on
24-`kind`. `Interrupted` means the engine stopped, the room closed, or the task
25-doing the work died before it could answer. `challenge` turns up with
23+one error type for everything, `Error { kind, message, challenge, server }`.
24+match on `kind`. `Interrupted` means the engine stopped, the room closed, or the
25+task doing the work died before it could answer. `challenge` turns up with
2626 `InteractiveAuthRequired`
2727
28+`message` is ours and says what we were doing. `server` is the homeserver's
29+errcode and message, dug out of the source chain when the error is built, so a
30+host can show "invalid password" without walking the chain. `AccountError`
31+carries the same, plus a `kind`. neither folds it into `Display`, since anything
32+printing the chain would say it twice
33+
2834 ## engine
2935
3036 the run loop routes and does nothing slow. every task it spawns is registered
Modified file: src/account.rs+18 additions−1 deletions
@@ -25,7 +25,8 @@ use tokio::sync::{broadcast, mpsc};
2525 use url::Url;
2626
2727 use crate::{
28- ClientHandle, Engine, EngineConfig, EngineError, ErrorKind, EventStream, SessionStore,
28+ ClientHandle, Engine, EngineConfig, EngineError, ErrorKind, EventStream, ServerError,
29+ SessionStore,
2930 engine::{Persistence, StoreLock},
3031 error::SourceError,
3132 };
@@ -348,6 +349,8 @@ pub enum AccountError {
348349 #[error("{context} failed")]
349350 Operation {
350351 context: &'static str,
352+ kind: ErrorKind,
353+ server: Option<ServerError>,
351354 #[source]
352355 source: Box<SourceError>,
353356 },
@@ -376,6 +379,18 @@ pub enum AccountError {
376379 Engine(#[from] EngineError),
377380 }
378381
382+impl AccountError {
383+ /// what the homeserver said, when it refused
384+ #[must_use]
385+ pub fn server(&self) -> Option<&ServerError> {
386+ match self {
387+ Self::Operation { server, .. } => server.as_ref(),
388+ Self::Unsaved { source, .. } => source.server(),
389+ _ => None,
390+ }
391+ }
392+}
393+
379394 /// a live login whose record never reached its `SessionStore`
380395 pub struct UnsavedSession {
381396 session: Session,
@@ -684,6 +699,8 @@ pub(crate) fn account_error(
684699 ) -> AccountError {
685700 AccountError::Operation {
686701 context,
702+ kind: crate::error::classify(&source),
703+ server: ServerError::find(&source),
687704 source: Box::new(source),
688705 }
689706 }
Modified file: src/error.rs+68 additions
@@ -33,6 +33,8 @@ pub struct Error {
3333 pub message: String,
3434 /// set when the server wants interactive auth before it'll do the thing
3535 pub challenge: Option<Box<InteractiveAuthChallenge>>,
36+ /// set when the homeserver refused with a matrix error
37+ pub server: Option<ServerError>,
3638 #[source]
3739 #[cfg_attr(feature = "serde", serde(skip))]
3840 source: Option<Arc<SourceError>>,
@@ -68,12 +70,76 @@ pub enum ErrorKind {
6870 Other,
6971 }
7072
73+/// the homeserver's own answer, worth showing a person as it stands
74+#[derive(Clone, Debug, Eq, PartialEq)]
75+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
76+#[non_exhaustive]
77+pub struct ServerError {
78+ pub status: u16,
79+ pub errcode: String,
80+ pub message: String,
81+}
82+
83+impl ServerError {
84+ pub(crate) fn find(source: &(dyn std::error::Error + 'static)) -> Option<Self> {
85+ let mut current = Some(source);
86+ while let Some(error) = current {
87+ let api = if let Some(error) = error.downcast_ref::<matrix_sdk::Error>() {
88+ error.as_client_api_error()
89+ } else if let Some(error) = error.downcast_ref::<HttpError>() {
90+ error.as_client_api_error()
91+ } else {
92+ error.downcast_ref::<ruma::api::error::Error>()
93+ };
94+ if let Some(api) = api
95+ && let ruma::api::error::ErrorBody::Standard(body) = &api.body
96+ {
97+ return Some(Self {
98+ status: api.status_code.as_u16(),
99+ errcode: body.kind.errcode().to_string(),
100+ message: body.message.clone(),
101+ });
102+ }
103+ current = error.source();
104+ }
105+ None
106+ }
107+}
108+
109+impl std::fmt::Display for ServerError {
110+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111+ write!(formatter, "{} ({})", self.message, self.errcode)
112+ }
113+}
114+
115+#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
116+pub(crate) fn classify(source: &(dyn std::error::Error + 'static)) -> ErrorKind {
117+ let mut current = Some(source);
118+ while let Some(error) = current {
119+ if let Some(error) = error.downcast_ref::<matrix_sdk::Error>() {
120+ return classify_sdk_error(error);
121+ }
122+ if let Some(error) = error.downcast_ref::<HttpError>() {
123+ return classify_http_error(error);
124+ }
125+ if let Some(error) = error.downcast_ref::<OAuthError>() {
126+ return classify_oauth_error(error);
127+ }
128+ if let Some(error) = error.downcast_ref::<RefreshTokenError>() {
129+ return classify_refresh_token_error(error);
130+ }
131+ current = error.source();
132+ }
133+ ErrorKind::Other
134+}
135+
71136 impl Error {
72137 pub(crate) fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
73138 Self {
74139 kind,
75140 message: message.into(),
76141 challenge: None,
142+ server: None,
77143 source: None,
78144 }
79145 }
@@ -84,6 +150,7 @@ impl Error {
84150 source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
85151 ) -> Self {
86152 Self {
153+ server: ServerError::find(&source),
87154 source: Some(Arc::new(source)),
88155 ..Self::new(kind, message)
89156 }
@@ -174,6 +241,7 @@ impl PartialEq for Error {
174241 self.kind == other.kind
175242 && self.message == other.message
176243 && self.challenge == other.challenge
244+ && self.server == other.server
177245 }
178246 }
179247
Modified file: src/lib.rs+1 additions−1 deletions
@@ -34,7 +34,7 @@ pub use account::{
3434 pub use session_store::{EncryptedSessionStore, SessionStore};
3535
3636 pub use engine::{Engine, EngineConfig, EngineError};
37-pub use error::{Error, ErrorKind};
37+pub use error::{Error, ErrorKind, ServerError};
3838 pub use imbl;
3939 #[cfg(feature = "mirror")]
4040 pub use mirror::{