Контекст и метаданные
masterror заменяет контекст, склеенный из строк (format!("failed to X: {e}")), тремя структурированными механизмами: билдером Context, типизированными полями Metadata и политиками редактирования, применяемыми на транспортной границе.
ResultExt: продвижение внешних ошибок
ResultExt реализован для каждого Result<T, E>, где E: Error + Send + Sync + 'static, и предлагает два метода:
.context(msg) — в стиле anyhow
Оборачивает ошибку сообщением; исходная ошибка становится источником:
use masterror::ResultExt;
fn read_config() -> Result<String, std::io::Error> {
Err(std::io::Error::from(std::io::ErrorKind::NotFound))
}
let err = read_config().context("Failed to read config file").unwrap_err();
assert!(err.source_ref().is_some());
Если нижележащая ошибка уже является masterror::Error, .context() сохраняет её классификацию: вид, код, метаданные, политика редактирования, подсказка повторной попытки и детали переносятся, заменяется только сообщение, а исходная ошибка сохраняется как источник.
.ctx(|| Context) — полный контроль
use masterror::{AppErrorKind, Context, ResultExt, field};
fn validate() -> Result<(), std::io::Error> {
Err(std::io::Error::other("boom"))
}
let err = validate()
.ctx(|| {
Context::new(AppErrorKind::Validation)
.with(field::str("phase", "validate"))
.redact(true)
.track_caller()
})
.unwrap_err();
assert_eq!(err.kind, AppErrorKind::Validation);
assert!(err.metadata().get("phase").is_some());
Замыкание вычисляется только на пути ошибки.
Билдер Context
| Метод | Эффект |
|---|---|
Context::new(kind) | Целевая категория; AppCode по умолчанию берётся из канонического отображения для этого вида |
.code(AppCode) | Переопределить публичный код |
.category(kind) | Сменить категорию; код остаётся синхронизированным, если не был переопределён |
.with(field) | Прикрепить Field метаданных |
.redact(bool) | Переключить редактирование сообщения (MessageEditPolicy::Redact / Preserve) |
.redact_field(name, FieldRedaction) | Переопределить политику редактирования именованного поля |
.track_caller() | Записать место вызова в метаданные caller.file, caller.line, caller.column |
Поля метаданных
Metadata — отсортированная карта типизированных полей с inline-размещением (0–4 поля остаются на стеке). Создавайте поля через модуль masterror::field:
| Конструктор | Вариант FieldValue |
|---|---|
field::str("key", value) | Str(Cow<'static, str>) |
field::i64("key", -1) | I64 |
field::u64("key", 42) | U64 |
field::f64("key", 0.5) | F64 |
field::bool("key", true) | Bool |
field::uuid("key", uuid) | Uuid |
field::duration("key", dur) | Duration |
field::ip("key", addr) | Ip (v4 или v6) |
field::json("key", json!({...})) | Json (требует функции serde_json) |
Прикрепляйте поля при создании ошибок или через Context:
use core::time::Duration;
use masterror::{AppError, FieldValue, field};
let err = AppError::service("downstream degraded")
.with_field(field::str("request_id", "abc123"))
.with_field(field::duration("elapsed", Duration::from_millis(1500)))
.with_field(field::u64("attempt", 2));
assert_eq!(err.metadata().len(), 3);
assert_eq!(err.metadata().get("attempt"), Some(&FieldValue::U64(2)));
for (name, value) in err.metadata().iter() {
println!("{name}={value}");
}
with_fields(iter) расширяет карту из итератора, with_metadata(meta) заменяет контейнер, а Metadata::insert возвращает прежнее значение при перезаписи ключа.
Политики редактирования
Политика сообщения: MessageEditPolicy
Preserve (по умолчанию) сохраняет публичное сообщение; Redact велит транспортам удалить его. Устанавливается через .redactable() на ошибке, .redact(true) на Context или redact(message) в #[masterror(...)]:
use masterror::{AppError, MessageEditPolicy, ProblemJson};
let err = AppError::internal("db-3 credentials rejected").redactable();
assert_eq!(err.edit_policy, MessageEditPolicy::Redact);
let problem = ProblemJson::from_app_error(err);
assert!(problem.detail.is_none());
Политика поля: FieldRedaction
Каждое поле несёт собственную политику, применяемую при сериализации метаданных в ProblemJson:
| Политика | Эффект на публичную нагрузку |
|---|---|
None | Значение сохраняется как есть |
Redact | Поле удаляется целиком |
Hash | Значение заменяется дайджестом SHA-256 |
Last4 | Маскируется всё, кроме последних четырёх символов |
use masterror::{AppError, FieldRedaction, field};
let err = AppError::internal("payment failed")
.with_field(field::str("card_number", "4111111111111111"))
.redact_field("card_number", FieldRedaction::Last4);
Имена, похожие на секреты, автоматически получают безопасное значение по умолчанию при создании поля: имена, содержащие password, secret, authorization, cookie, session, jwt, bearer, otp, pin, по умолчанию получают Redact; имена в духе токенов и ключей (api_token, refresh_token, key, apikey) — Hash; сегменты карт/счетов в сочетании с числовым сегментом (card_number, iban_no, account_id) — Last4. Распознавание регистронезависимо. Явный redact_field/with_redaction всегда имеет приоритет.
Цепочки ошибок
Ошибки сохраняют полную причинную цепочку. chain() итерирует от самой ошибки до первопричины; root_cause() сразу переходит к самой глубокой ошибке:
use masterror::AppError;
let io_err = std::io::Error::other("disk offline");
let app_err = AppError::internal("db down").with_context(io_err);
let chain: Vec<_> = app_err.chain().collect();
assert_eq!(chain.len(), 2);
assert_eq!(app_err.root_cause().to_string(), "disk offline");
with_context(...) — предпочтительный способ прикрепить вышестоящую ошибку: он принимает владеемые ошибки или разделяемые значения Arc<dyn Error + Send + Sync> и переиспользует существующие аллокации. with_source(...) / with_source_arc(...) — их низкоуровневые эквиваленты.
Даункастинг
Проверяйте прикреплённый источник через is и downcast_ref/downcast_mut:
use masterror::AppError;
let io_err = std::io::Error::other("disk offline");
let err = AppError::internal("boom").with_context(io_err);
assert!(err.is::<std::io::Error>());
if let Some(io) = err.downcast_ref::<std::io::Error>() {
assert_eq!(io.to_string(), "disk offline");
}
is::<E>()—true, когда непосредственный источник имеет типE(не обходит всю цепочку).downcast_ref::<E>()— заимствует источник какE.downcast::<E>()/downcast_mut::<E>()— пока заглушки (downcastвсегда возвращаетErr(self),downcast_mutвсегда возвращаетNone), поэтому предпочитайтеdowncast_ref.
Для более глубоких проверок обходите chain() и вызывайте source.is::<E>() / source.downcast_ref::<E>() на каждом элементе.
Бэктрейсы
С функцией backtrace метод err.backtrace() возвращает лениво захваченный std::backtrace::Backtrace, учитывающий RUST_BACKTRACE, а with_backtrace(bt) прикрепляет явно захваченный. Бэктрейсы разделяются через Arc при переоборачивании ошибок, поэтому цепочки .context() не выполняют повторный захват.
См. также: Начало работы · Виды и коды ошибок · Derive-макросы · Наблюдаемость · Лучшие практики