From 79bc1981657b469b42b460b66650aa9e60171058 Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 16 Aug 2026 00:36:54 +0200 Subject: [PATCH] fix(swarm-queue-client): export chain, and use it where anyhow used to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review catch: `anyhow::Error`'s Display special-cases `f.alternate()` to walk the source chain; thiserror's derive does not, so `{e:#}` and `{e}` render identically for the new error type. Every call site that held an `anyhow::Error`, formatted it with `{:#}`, and now holds this crate's error kept compiling, kept looking right, and silently dropped the cause. `chain()` was written for exactly this and then left private, applied only to the auth callback I happened to be editing. Its own doc comment argues that dropping the source chain is wrong, which made it the one thing in the PR that should not have had a scope of one. The controller's "swarm queue unreachable" warning is the site this fixes here; the stacked PR fixes the two boot-warning banners, which matter more still — one-shot, no retry, and they leak until restart. --- swarm-controller/src/main.rs | 8 +++++++- swarm-queue-client/src/lib.rs | 20 ++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index d8bdc899..3c9e675e 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -318,7 +318,13 @@ async fn main() -> Result<()> { ))) } Err(e) => { - tracing::warn!(error = format!("{e:#}"), "swarm queue unreachable"); + // `chain`, not `{:#}`: this is the queue client's own + // error type, and thiserror's Display ignores the + // alternate flag — the source would be dropped silently. + tracing::warn!( + error = swarm_queue_client::chain(&e), + "swarm queue unreachable" + ); None } }, diff --git a/swarm-queue-client/src/lib.rs b/swarm-queue-client/src/lib.rs index 29def273..94a4af0d 100644 --- a/swarm-queue-client/src/lib.rs +++ b/swarm-queue-client/src/lib.rs @@ -88,12 +88,20 @@ pub enum Error { /// Render an error and its source chain on one line. /// -/// The auth callback below hands `async-nats` a *string*, so a `Display` -/// that stopped at the top message would drop the cause — which is the -/// half that says why the mint failed. `anyhow`'s `{:#}` did this for -/// free; a library owes its callers the same detail without owing them -/// anyhow. -fn chain(error: &dyn std::error::Error) -> String { +/// **Use this instead of `{:#}` anywhere an [`Error`] is rendered without +/// first being `?`-converted into an `anyhow::Error`.** `anyhow`'s +/// `Display` special-cases `f.alternate()` to walk the source chain; +/// `thiserror`'s derive does not, so `{e:#}` and `{e}` render +/// identically for this type. A call site that formatted an +/// `anyhow::Error` with `{:#}` and now holds an [`Error`] therefore keeps +/// compiling, keeps looking right, and silently drops the cause — which +/// is the half that says *why*, and is exactly what a one-shot boot +/// warning with no retry needs most. +/// +/// Public for that reason: the fix cannot live only inside this crate's +/// own auth callback while the callers it was written for reach for +/// `{:#}` and get nothing. +pub fn chain(error: &dyn std::error::Error) -> String { let mut rendered = error.to_string(); let mut source = error.source(); while let Some(cause) = source {