diff --git a/crates/pstr-android/src/lib.rs b/crates/pstr-android/src/lib.rs index cfdf88e..9703d48 100644 --- a/crates/pstr-android/src/lib.rs +++ b/crates/pstr-android/src/lib.rs @@ -2637,11 +2637,11 @@ impl AndroidEngine { if let Err(error) = self .catalog .lock() - .record_crawl_failure(&id, &error.to_string()) + .record_crawl_failure(&id, &error.describe()) { log::warn!("record a failed crawl: {error}"); } - return Err(BridgeError::from_display(error)); + return Err(BridgeError::from_display(error.describe())); } }; let rows = build_rows(&id, &nodes); diff --git a/crates/pstr-app/src/engine.rs b/crates/pstr-app/src/engine.rs index 0dc798b..5e3a98d 100644 --- a/crates/pstr-app/src/engine.rs +++ b/crates/pstr-app/src/engine.rs @@ -1494,7 +1494,7 @@ impl Engine { if let Err(error) = self .catalog .lock() - .record_crawl_failure(&share_id, &error.to_string()) + .record_crawl_failure(&share_id, &error.describe()) { tracing::warn!(%error, "record a failed crawl"); } diff --git a/crates/pstr-core/src/error.rs b/crates/pstr-core/src/error.rs index a643eff..7939046 100644 --- a/crates/pstr-core/src/error.rs +++ b/crates/pstr-core/src/error.rs @@ -54,6 +54,36 @@ pub enum Error { } impl Error { + /// The message, followed by every cause its own text leaves out. + /// + /// A dropped connection reads as reqwest's "error sending request for url + /// (…)" and nothing else: why — a reset, a DNS failure, a certificate — is + /// further down the source chain. That is the part a viewer reporting a + /// failed crawl needs, so the URL, which only says which folder, is left + /// out to keep it on screen. + pub fn describe(&self) -> String { + let mut text = match self { + Self::Drive(proton_sdk::error::ProtonError::Transport(error)) => { + let url = error.url().map(|url| format!(" for url ({url})")); + let own = self.to_string(); + url.map_or_else(|| own.clone(), |url| own.replace(&url, "")) + } + _ => self.to_string(), + }; + let mut seen = self.to_string(); + let mut source = std::error::Error::source(self); + while let Some(cause) = source { + let line = cause.to_string(); + if !seen.contains(&line) { + text.push_str(": "); + text.push_str(&line); + seen.push_str(&line); + } + source = cause.source(); + } + text + } + /// Whether this is the account's session having ended: the refresh token /// was revoked or expired, or the token lacks a scope only a password /// sign-in grants. Nothing but signing in again fixes it. @@ -76,3 +106,25 @@ impl From for Error { } pub type Result = std::result::Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, thiserror::Error)] + #[error("connection closed before message completed")] + struct Closed; + + #[derive(Debug, thiserror::Error)] + #[error("client error (SendRequest)")] + struct SendRequest(#[source] Closed); + + #[test] + fn a_failure_is_described_with_the_causes_its_own_text_leaves_out() { + let error = Error::Io(std::io::Error::other(SendRequest(Closed))); + assert_eq!( + error.describe(), + "io: client error (SendRequest): connection closed before message completed" + ); + } +}