Skip to content

Add a support footer

Register the closure

fn main() -> miette::Result<()> {
    rtb_error::hook::install_with_footer(|| {
        "Report bugs at https://gitlab.com/example/greet/-/issues".to_string()
    });
    rtb_error::hook::install_panic_hook();

    run()?;
    Ok(())
}

install_with_footer installs the report handler for you, so there is no need to call install_report_handler as well. Every rendered diagnostic now ends with a blank line and the footer:

Error: greet::no_name

  x no name given
  help: pass a name: `greet Ada`

Report bugs at https://gitlab.com/example/greet/-/issues

Panics get it too, as long as install_panic_hook was also called — they render through the same handler.

Set it before anything can fail

miette's hook slot is filled by the first Report constructed anywhere in the process. If that happens before your install call, this crate's handler is never installed and the footer is never read — with no error and no warning. Register the footer as the first statement in main.

Call install_with_footer again. The most recent closure wins, and the handler reads it at render time rather than install time, so the change applies to every diagnostic from that point on:

fn main() -> miette::Result<()> {
    rtb_error::hook::install_with_footer(|| {
        "Report bugs at https://gitlab.com/example/greet/-/issues".to_string()
    });

    let config = load_config()?;
    let channel = config.support_channel.clone();
    rtb_error::hook::install_with_footer(move || format!("Support: {channel}"));

    run()?;
    Ok(())
}

The closure must be Send + Sync + 'static, so anything it captures has to be owned — move and a cloned String, not a borrow of config.

Return an empty string. No blank line is emitted:

rtb_error::hook::install_with_footer(|| {
    if std::env::var_os("GREET_QUIET").is_some() {
        String::new()
    } else {
        "Report bugs at https://gitlab.com/example/greet/-/issues".to_string()
    }
});

The closure takes no arguments, so this is the only conditional available — it cannot see which diagnostic is being rendered.

Keep the closure trivial

It runs on every render, inside the rendering path, while a lock is held. If it panics, the panic is caught and the footer is dropped for that render, and nothing reports the suppression — the diagnostic body renders normally and the footer is simply absent. A footer that quietly stops appearing is very hard to trace back.

Format a string that is already in hand. Do not read a file, call a network, or unwrap an Option in there.

Test that it appears

Assert against Report's Debug output, which is the path that goes through the hook:

rtb_error::hook::install_with_footer(|| "support: #team".to_string());
let report = miette::Report::new(GreetError::NoName);
assert!(format!("{report:?}").contains("support: #team"));

GraphicalReportHandler::render_report called directly bypasses the hook and will never show the footer, which makes it the right tool for tests that want deterministic diagnostic output without one.

Because miette's hook is process-global and set-once, tests that install different hooks interfere with each other under a shared-process test runner. This repository uses cargo nextest, which runs each test in its own process; cargo test alone may give different results.