# redacted-error
Stable public error messages with debug-only diagnostic detail.
`redacted-error` is a small facade for Rust errors that cross crate, process,
API, or protocol boundaries. It lets applications expose stable public error
codes and messages while preserving rich diagnostics in debug builds.
## Features
- `message!` for displayable static public-facing messages.
- `message_string!` for owned public-facing messages.
- `detail!`, `detail`, and `display` for debug-only runtime diagnostics.
- `ErrorCode` and `PublicError` traits for stable public API responses.
- `impl_redacted_debug!` to keep release `Debug` from leaking enum fields.
- Optional static string obfuscation behind the default `obfuscate` feature.
The public API does not expose the obfuscation implementation. Today the
default backend is `obfstr`; later releases can replace that backend without
requiring callers to rename macros.
`message!` returns a `Message` wrapper, not a borrowed `&'static str`. That is
intentional: it keeps backend-specific lifetime behavior out of caller code.
Use `message_string!` when a trait or response type needs an owned `String`.
## Usage
```toml
[dependencies]
redacted-error = "0.1"
```
Disable static string obfuscation:
```toml
[dependencies]
redacted-error = { version = "0.1", default-features = false }
```
## Example
```rust
use redacted_error::message as m;
use thiserror::Error;
#[cfg_attr(debug_assertions, derive(Debug))]
#[derive(Error)]
pub enum TransportError {
#[cfg_attr(debug_assertions, error("{prefix} {0}", prefix = m!("listener bind failed:")))]
#[cfg_attr(not(debug_assertions), error("{}", m!("listener bind failed")))]
ListenerBindFailed(String),
}
redacted_error::impl_redacted_debug!(TransportError);
impl redacted_error::ErrorCode for TransportError {
fn code(&self) -> &'static str {
match self {
Self::ListenerBindFailed(_) => "transport.listener_bind_failed",
}
}
}
impl redacted_error::PublicError for TransportError {
fn public_message(&self) -> redacted_error::Message {
match self {
Self::ListenerBindFailed(_) => redacted_error::message!("listener bind failed"),
}
}
}
fn bind_failed(addr: &str, err: impl std::fmt::Display) -> TransportError {
TransportError::ListenerBindFailed(redacted_error::detail!(
"{addr}: {err}"
))
}
```
In debug builds, `Display` can include diagnostic detail:
```text
listener bind failed: 127.0.0.1:8080: address already in use
```
In release builds, `Display` and `Debug` stay public-safe:
```text
listener bind failed
```
For API responses, use stable structured fields rather than parsing `Display`:
```json
{
"code": "transport.listener_bind_failed",
"message": "listener bind failed"
}
```
## Security
This crate is a leakage-reduction tool, not a confidentiality boundary.
- The `obfuscate` feature only raises the bar against trivial `strings`-style
inspection of compiled binaries. It is **not** a defense against a debugger,
dynamic instrumentation, symbol tables, or any motivated reverse engineer.
Do not treat obfuscated literals as secret.
- Diagnostic-detail stripping is gated on `cfg(debug_assertions)`. Cargo's
standard `release` profile turns this off, but `[profile.release]
debug-assertions = true` re-enables it — and with it, every `detail!` /
`display` / `detail` call leaks runtime detail in release builds. Avoid
that combination if redaction matters.
- The `detail!` macro skips evaluating its format arguments in release. The
`detail` and `display` free functions still evaluate (and drop) their
argument; prefer the macro when the argument has nontrivial cost or side
effects.
## Migration Notes
For existing code using `obfstr` in error messages:
```rust
use obfstr::obfstr as s;
```
Prefer:
```rust
use redacted_error::message as s;
```
Then replace dynamic error details with `redacted_error::detail!` or
`redacted_error::display`.
For code currently using the local `error-detail` crate, the intended mapping
is:
| `error_detail::obfstr!("...")` | `redacted_error::message!("...")` |
| `error_detail::obfstring!("...")` | `redacted_error::message_string!("...")` |
| `error_detail::detail!("...")` | `redacted_error::detail!("...")` |
| `error_detail::display(err)` | `redacted_error::display(err)` |
| `error_detail::ErrorCode` | `redacted_error::ErrorCode` |
| `error_detail::impl_redacted_debug!` | `redacted_error::impl_redacted_debug!` |
## License
Licensed under either of Apache License, Version 2.0 or MIT license at your
option.