Skip to content

fix(server): log the full anyhow cause chain at mailer failure sites - #331

Merged
ntatschner merged 1 commit into
nextfrom
fix/mail-error-cause-chain
Jul 26, 2026
Merged

fix(server): log the full anyhow cause chain at mailer failure sites#331
ntatschner merged 1 commit into
nextfrom
fix/mail-error-cause-chain

Conversation

@ntatschner

Copy link
Copy Markdown
Collaborator

Display on an anyhow::Error prints only the outermost context. Every mail path wraps its errors (.context("parse To address"), .context("SMTP send failed"), …), so error = %e has been logging the wrapper and silently discarding the actual cause.

That is why the waitlist invite outage read as a broken transport for as long as it did. The logs only ever said:

waitlist invite RESEND failed — the mail transport is still broken   error="parse To address"

The cause — that a display name containing @ is not a valid RFC 5322 atom — was one context layer down and never printed. The transport was fine the whole time.

Change

The 8 sites that log a Mailer error now use %format!("{e:#}"). The alternate Display form flattens the whole chain onto one line:

error="send_waitlist_invite failed: parse To address: invalid email address: a@b.com <a@b.com>"

Single-line matters. ?e (Debug) also carries the chain but emits multi-line output with Caused by: stanzas, which is hostile to the JSON log pipeline.

File Sites
waitlist_routes.rs 3 (invite on signup, admit, resend)
auth_routes.rs 4 (verification ×2, password reset, email-change verify)
magic_link_routes.rs 1
smtp_admin_routes.rs 1 (see below)

Not a new idiom. format!("{e:#}") is already used for exactly this at smtp_admin_routes.rs:351 and telemetry.rs:63. The SMTP test-send site is included because its code used ?e while the comment directly above it prescribed {e:#} — code and comment now agree.

Deliberately not a repo-wide sweep

There are 392 error = %e sites. Most log a thiserror enum whose Display already interpolates its source — e.g. WaitlistError::Database(#[from] sqlx::Error) renders as "database error: {0}" — so {:#} would add nothing there. Only anyhow errors lose information this way, and the Mailer trait (anyhow::Result) is where it actually bit. Each of the 8 edits was verified by its surrounding context to be a mailer call site, not just by line number.

Test

alternate_display_surfaces_the_cause_chain_that_plain_display_hides pins the difference directly: plain Display yields only "send_waitlist_invite failed" and demonstrably hides the root cause, while {e:#} carries every layer down to it — and stays single-line.

cargo test -p starstats-server 960 + 932, clippy -D warnings and cargo fmt --all --check clean. No migration, no behaviour change beyond log content.

`Display` on an anyhow::Error prints ONLY the outermost context, so
`error = %e` logs the wrapper and silently discards the actual cause.
Every mail path wraps its errors (`.context("parse To address")`,
`.context("SMTP send failed")`, …), so every mail failure has been
logged as its wrapper alone.

That is why the waitlist invite outage read as a broken transport for as
long as it did: the logs only ever said `parse To address`, never which
address or why. The cause — that a display name containing `@` is not a
valid RFC 5322 atom — was one context layer down and never printed.

Switches the 8 sites that log a `Mailer` error to `%format!("{e:#}")`,
whose alternate Display flattens the whole chain onto ONE line. Single
line matters: `?e` (Debug) also carries the chain but emits multi-line
output, which is hostile to the JSON log pipeline.

This is not a new idiom — `format!("{e:#}")` is already used for exactly
this in smtp_admin_routes.rs:351 and telemetry.rs:63. The SMTP test-send
site is included here because its code used `?e` while the comment
directly above it prescribed `{e:#}`; code and comment now agree.

Deliberately NOT a repo-wide sweep. There are 392 `error = %e` sites;
most log a thiserror enum whose Display already interpolates its source
(e.g. `WaitlistError::Database(#[from] sqlx::Error)` renders as
"database error: {0}"), so `{:#}` would add nothing. Only anyhow errors
lose information this way, and the Mailer trait is where that bit.

Adds a test pinning the difference: plain Display yields only
"send_waitlist_invite failed" while `{e:#}` carries every layer down to
the root cause, and stays single-line.
@ntatschner
ntatschner merged commit 664790b into next Jul 26, 2026
12 checks passed
@ntatschner
ntatschner deleted the fix/mail-error-cause-chain branch July 26, 2026 20:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant