fix(swarm-queue-client): export chain, and use it where anyhow used to

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.
This commit is contained in:
atlas 2026-08-16 00:36:54 +02:00 committed by mara
commit 79bc198165
2 changed files with 21 additions and 7 deletions

View file

@ -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
}
},

View file

@ -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 {