diff --git a/README.ru.md b/README.ru.md
index a1c46b9..7ee1b28 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -14,12 +14,13 @@ SPDX-License-Identifier: MIT
[](https://crates.io/crates/masterror)
[](https://docs.rs/masterror)
[](https://crates.io/crates/masterror)
- 
- 
+ 
+ 
+ [](https://api.reuse.software/info/github.com/RAprogramm/masterror)
[](https://codecov.io/gh/RAprogramm/masterror)
[](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml?query=branch%3Amain)
- [](https://hitsofcode.com/github/RAprogramm/masterror/view?branch=main)
+ [](https://hitsofcode.com/github/RAprogramm/masterror/view?branch=main&exclude=Cargo.lock,.gitignore,CHANGELOG.md)
[](https://github.com/RAprogramm/infra-metrics-insight-renderer)
> 🇬🇧 [Read README in English](README.md)
@@ -43,6 +44,7 @@ SPDX-License-Identifier: MIT
- [Покрытие кода](#покрытие-кода)
- [Быстрый старт](#быстрый-старт)
- [Расширенное использование](#расширенное-использование)
+- [Примеры](#примеры)
- [Ресурсы](#ресурсы)
- [Метрики](#метрики)
- [Лицензия](#лицензия)
@@ -67,12 +69,12 @@ SPDX-License-Identifier: MIT
## Особенности
-- **Унифицированная таксономия.** `AppError`, `AppErrorKind` и `AppCode` моделируют доменные и транспортные задачи с консервативными отображениями HTTP/gRPC, готовыми подсказками для повторных попыток и аутентификации, а также вывода RFC7807 через `ProblemJson`.
-- **Нативные derive-макросы.** `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]`, `#[masterror(...)]` и `#[provide]` встраивают пользовательские типы в `AppError`, передавая источники, бэктрейсы, провайдеры телеметрии и политики редактирования.
+- **Унифицированная таксономия.** `AppError`, `AppErrorKind` и `AppCode` моделируют доменные и транспортные задачи с консервативными отображениями HTTP/gRPC, готовыми подсказками для повторных попыток и аутентификации, а также выводом RFC7807 через `ProblemJson`.
+- **Нативные derive-макросы.** `#[derive(Error)]` и `#[derive(Masterror)]` встраивают пользовательские типы в типы времени выполнения. `#[derive(Masterror)]` с `#[masterror(...)]` пробрасывает источники, бэктрейсы, поля телеметрии и политику редактирования; `#[app_error]` отображает произведенную ошибку в `AppErrorKind`/`AppCode` (опционально раскрывая её сообщение `Display`), а `#[provide]` регистрирует типизированные провайдеры телеметрии на самой доменной ошибке.
- **Типизированная телеметрия.** `Metadata` хранит структурированный контекст в формате ключ/значение (строки, целые числа, числа с плавающей точкой, длительности, IP-адреса и опциональный JSON) с контролем редактирования для каждого поля и конструкторами в `field::*`, чтобы логи оставались структурированными без ручного создания `String`-карт.
- **Транспортные адаптеры.** Опциональные функции предоставляют респондеры Actix/Axum, конверсии в `tonic::Status`, логирование WASM/браузера и генерацию схемы OpenAPI без загрязнения компактной сборки по умолчанию.
-- **Проверенные интеграции.** Включите точечные отображения для `sqlx`, `reqwest`, `redis`, `validator`, `config`, `tokio`, `teloxide`, `multipart`, Telegram WebApp SDK и других — каждое из них транслирует библиотечные ошибки в таксономию с присоединенной телеметрией.
-- **Готовые значения по умолчанию.** Модуль `turnkey` поставляет готовый к использованию каталог ошибок, вспомогательные конструкторы и инструментарий трассировки для команд, которые хотят получить согласованную базовую линию из коробки.
+- **Проверенные интеграции.** Включите точечные отображения для `sqlx`, `reqwest`, `redis`, `validator`, `config`, `tokio`, `teloxide`, `multipart`, валидации Telegram init-data и других — каждое из них транслирует библиотечные ошибки в таксономию с присоединенной телеметрией.
+- **Готовые значения по умолчанию.** Модуль `turnkey` поставляет готовый к использованию каталог ошибок, эвристический классификатор и консервативные отображения в каноническую таксономию для команд, которые хотят получить согласованную базовую линию из коробки.
- **Типизированные макросы управления потоком.** `ensure!` и `fail!` прерывают выполнение функций с доменными ошибками без выделения памяти или форматирования на успешном пути.
@@ -109,13 +111,13 @@ SPDX-License-Identifier: MIT
## Флаги функций
-Выбирайте только то, что вам нужно; по умолчанию все отключено.
+Выбирайте только то, что вам нужно; по умолчанию включён только `std`, всё остальное подключается явно.
- **Веб-транспорты:** `axum`, `actix`, `multipart`, `openapi`, `serde_json`.
- **Телеметрия и наблюдаемость:** `tracing`, `metrics`, `backtrace`, `colored` для
цветного вывода в терминале.
- **Асинхронные интеграции и ввод/вывод:** `tokio`, `reqwest`, `sqlx`, `sqlx-migrate`, `redis`, `validator`, `config`.
-- **Обмен сообщениями и боты:** `teloxide`, `telegram-webapp-sdk`.
+- **Обмен сообщениями и боты:** `teloxide`, `init-data` для валидации init-data Telegram Mini App через `init-data-rs`.
- **Инструменты фронтенда:** `frontend` для логирования WASM/консоли браузера.
- **gRPC:** `tonic` для отправки ответов `tonic::Status`.
- **Всё включено:** `turnkey` для принятия готовой таксономии и вспомогательных функций.
@@ -138,15 +140,15 @@ SPDX-License-Identifier: MIT
~~~toml
[dependencies]
-masterror = { version = "0.24.19", default-features = false }
-# или с функциями:
-# masterror = { version = "0.24.19", features = [
+masterror = { version = "0.28.0", default-features = false }
+# or with features:
+# masterror = { version = "0.28.0", features = [
# "std", "axum", "actix", "openapi",
# "serde_json", "tracing", "metrics", "backtrace",
-# "sqlx", "sqlx-migrate", "reqwest", "redis",
-# "validator", "config", "tokio", "multipart",
-# "teloxide", "telegram-webapp-sdk", "tonic", "frontend",
-# "turnkey", "benchmarks"
+# "colored", "sqlx", "sqlx-migrate", "reqwest",
+# "redis", "validator", "config", "tokio",
+# "multipart", "teloxide", "init-data", "tonic",
+# "frontend", "turnkey", "benchmarks"
# ] }
~~~
@@ -172,7 +174,7 @@ cargo bench -F benchmarks --bench error_paths
Набор тестов выдает две группы:
-- `context_into_error/*` продвигает фиктивную исходную ошибку с репрезентативными метаданными (строки, счетчики, длительности, IP-адреса) через `Context::into_error` в режимах с редактированием и без редактирования.
+- `context_into_error/*` продвигает фиктивную исходную ошибку с репрезентативными метаданными (строки, счетчики, длительности, IP-адреса) через `ResultExt::ctx` в режимах с редактированием и без редактирования.
- `problem_json_from_app_error/*` использует результирующие значения `AppError` для построения полезных нагрузок RFC 7807 через `ProblemJson::from_app_error`, показывая, как редактирование сообщений и политики полей влияют на сериализацию.
Настройте флаги командной строки Criterion (например, `--sample-size 200` или `--save-baseline local`) после `--` для обмена пропускной способности на более точные доверительные интервалы при исследовании изменений.
@@ -412,7 +414,9 @@ assert_eq!(
Структурированные провайдеры телеметрии и отображения AppError
-`#[provide(...)]` предоставляет типизированный контекст через `std::error::Request`, в то время как `#[app_error(...)]` записывает, как ваша доменная ошибка транслируется в `AppError` и `AppCode`. Derive отражает синтаксис `thiserror` и расширяет его опциональным распространением телеметрии и прямыми конверсиями в типы времени выполнения `masterror`.
+`#[provide(...)]` предоставляет типизированный контекст через `std::error::Request`, в то время как `#[app_error(...)]` записывает, как ваша доменная ошибка транслируется в `AppError` и `AppCode`. Derive отражает синтаксис `thiserror`. Обратите внимание, что сгенерированные конверсии `From` создают `AppError`, несущий только отображенные kind и code (плюс вывод `Display` как публичное сообщение, если установлен флаг `message`) — исходная доменная ошибка отбрасывается, поэтому её источники и провайдеры телеметрии не пробрасываются. Запрашивайте телеметрию у доменной ошибки до конверсии.
+
+`request_ref`/`request_value` и механизм `std::error::Request` требуют nightly-тулчейна (`error_generic_member_access`); крейт определяет поддержку компилятора во время сборки и включает интеграцию провайдеров только когда она доступна.
~~~rust
use std::error::request_ref;
@@ -444,8 +448,7 @@ let snapshot = request_ref::(&err).expect("telemetry");
assert_eq!(snapshot.value, 42);
let app: AppError = err.into();
-let via_app = request_ref::(&app).expect("telemetry");
-assert_eq!(via_app.name, "db.query");
+assert!(matches!(app.kind, AppErrorKind::Service));
~~~
Опциональная телеметрия появляется только когда присутствует, поэтому `None` не регистрирует провайдера. Собственные снимки все еще могут быть предоставлены как значения, когда вызывающая сторона запрашивает владение:
@@ -510,11 +513,10 @@ assert!(matches!(app.kind, AppErrorKind::Service));
~~~rust
use masterror::{AppError, AppErrorKind, ProblemJson};
-use std::time::Duration;
let problem = ProblemJson::from_app_error(
AppError::new(AppErrorKind::Unauthorized, "Token expired")
- .with_retry_after_duration(Duration::from_secs(30))
+ .with_retry_after_secs(30)
.with_www_authenticate(r#"Bearer realm="api", error="invalid_token""#)
);
@@ -526,85 +528,58 @@ assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED");
- Форматирование ошибок с учётом окружения через DisplayMode
-
-API `DisplayMode` позволяет контролировать форматирование вывода ошибок в зависимости от окружения развертывания без изменения кода обработки ошибок. Доступны три режима:
-
-- **`DisplayMode::Prod`** — Компактный JSON-вывод с минимальным набором полей, оптимизированный для production логов. Включает только `kind`, `code` и `message` (если не скрыто). Автоматически фильтрует чувствительные метаданные.
-
-- **`DisplayMode::Local`** — Человекочитаемый многострочный вывод с полным контекстом. Показывает детали ошибки, полную цепочку источников, все метаданные и бэктрейс (если включен). Лучший вариант для локальной разработки и отладки.
+ Определение окружения через DisplayMode
-- **`DisplayMode::Staging`** — JSON-вывод с дополнительным контекстом. Включает `kind`, `code`, `message`, ограниченную `source_chain` и отфильтрованные метаданные. Полезен для staging окружений, где нужны структурированные логи с большей детализацией.
+`DisplayMode` определяет окружение развертывания (`Prod`, `Local` или
+`Staging`), чтобы ваш код мог ветвиться по нему. `DisplayMode::current()`
+разрешает режим в следующем порядке и кэширует результат при первом обращении:
-**Автоматическое определение окружения:**
-
-Режим определяется автоматически в следующем порядке:
1. Переменная окружения `MASTERROR_ENV` (`prod`, `local` или `staging`)
-2. Наличие `KUBERNETES_SERVICE_HOST` (активирует режим `Prod`)
+2. Наличие `KUBERNETES_SERVICE_HOST` (выбирает `Prod`)
3. Конфигурация сборки (`debug_assertions` → `Local`, release → `Prod`)
-Результат кэшируется при первом обращении для нулевой стоимости последующих вызовов.
-
~~~rust
use masterror::DisplayMode;
-// Запрос текущего режима (кэшируется после первого вызова)
let mode = DisplayMode::current();
match mode {
- DisplayMode::Prod => println!("Работа в production режиме"),
- DisplayMode::Local => println!("Работа в режиме локальной разработки"),
- DisplayMode::Staging => println!("Работа в staging режиме"),
+ DisplayMode::Prod => println!("Running in production mode"),
+ DisplayMode::Local => println!("Running in local development mode"),
+ DisplayMode::Staging => println!("Running in staging mode"),
}
~~~
+Примечание: `Display` для `AppError` пока не учитывает `DisplayMode` — вывод
+идентичен во всех режимах. Единственная ветка форматирования сегодня — функция
+`colored`, описанная ниже; форматирование с учётом режима не подключено.
+
**Цветной вывод в терминале:**
-Включите функцию `colored` для улучшенного вывода в терминале в локальном режиме:
+Включите функцию `colored` для улучшенного вывода в терминале. Она применяется
+всегда, когда функция включена, независимо от определённого режима:
~~~toml
[dependencies]
-masterror = { version = "0.24.19", features = ["colored"] }
+masterror = { version = "0.28.0", features = ["colored"] }
~~~
-С включённой `colored`, ошибки отображаются с подсветкой синтаксиса:
-- Тип и код ошибки выделены жирным шрифтом
-- Сообщения об ошибках в цвете
-- Цепочка источников с отступами
-- Выделенные ключи метаданных
-
-~~~rust
-use masterror::{AppError, field};
-
-let error = AppError::not_found("Пользователь не найден")
- .with_field(field::str("user_id", "12345"))
- .with_field(field::str("request_id", "abc-def"));
-
-// Без 'colored': обычный текст
-// С 'colored': цветной вывод в терминалах
-println!("{}", error);
-~~~
-
-**Вывод в Production vs Development:**
-
Без функции `colored` ошибки отображают метку `AppErrorKind`:
~~~
NotFound
~~~
-С функцией `colored` полный многострочный формат с контекстом:
+С функцией `colored` — полный многострочный формат с контекстом:
~~~
Error: NotFound
Code: NOT_FOUND
-Message: Пользователь не найден
+Message: User not found
Context:
user_id: 12345
request_id: abc-def
~~~
-Такое разделение сохраняет production логи чистыми, предоставляя разработчикам богатый контекст во время локальных сессий отладки.
-
@@ -619,6 +594,31 @@ Context:
---
+## Примеры
+
+Полноценные примеры из реальной практики, демонстрирующие интеграцию masterror с популярными фреймворками:
+
+| Пример | Описание | Возможности |
+|---------|-------------|----------|
+| [**axum-rest-api**](examples/axum-rest-api/) | REST API с RFC 7807 Problem Details | HTTP-эндпоинты, доменные ошибки, интеграционные тесты |
+| [**sqlx-database**](examples/sqlx-database/) | Обработка ошибок базы данных с SQLx | Ошибки подключения, нарушения ограничений, транзакции |
+| [**custom-domain-errors**](examples/custom-domain-errors/) | Доменные ошибки обработки платежей | Derive-макрос, конверсия ошибок, структурированные ошибки |
+| [**basic-async**](examples/basic-async/) | Асинхронная обработка ошибок с tokio | Распространение ошибок, обработка таймаутов, типы Result |
+
+Все примеры запускаемы; пример axum-rest-api дополнительно поставляется с интеграционными тестами. Смотрите директорию [`examples/`](examples/) для полного исходного кода и документации.
+
+
+
+---
+
## Ресурсы
- Изучите [вики по обработке ошибок](https://github.com/RAprogramm/masterror/wiki) для пошаговых руководств, сравнений с `thiserror`/`anyhow` и рецептов решения проблем.
@@ -656,5 +656,4 @@ Context:
## Лицензия
-MSRV: **1.90** · Лицензия: **MIT OR Apache-2.0** · Без `unsafe`
-
+MSRV: **1.96** · Лицензия: **MIT** · Без `unsafe`
diff --git a/README.template.md b/README.template.md
index 381a842..b1ec72b 100644
--- a/README.template.md
+++ b/README.template.md
@@ -74,9 +74,12 @@ of redaction and metadata.
- **Unified taxonomy.** `AppError`, `AppErrorKind` and `AppCode` model domain and
transport concerns with conservative HTTP/gRPC mappings, turnkey retry/auth
hints and RFC7807 output via `ProblemJson`.
-- **Native derives.** `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]`,
- `#[masterror(...)]` and `#[provide]` wire custom types into `AppError` while
- forwarding sources, backtraces, telemetry providers and redaction policy.
+- **Native derives.** `#[derive(Error)]` and `#[derive(Masterror)]` wire custom
+ types into the runtime types. `#[derive(Masterror)]` with `#[masterror(...)]`
+ forwards sources, backtraces, telemetry fields and redaction policy;
+ `#[app_error]` maps a derived error to an `AppErrorKind`/`AppCode` (optionally
+ exposing its `Display` message) and `#[provide]` registers typed telemetry
+ providers on the domain error itself.
- **Typed telemetry.** `Metadata` stores structured key/value context (strings,
integers, floats, durations, IP addresses and optional JSON) with per-field
redaction controls and builders in `field::*`, so logs stay structured without
@@ -86,11 +89,11 @@ of redaction and metadata.
generation without contaminating the lean default build.
- **Battle-tested integrations.** Enable focused mappings for `sqlx`,
`reqwest`, `redis`, `validator`, `config`, `tokio`, `teloxide`, `multipart`,
- Telegram WebApp SDK and more — each translating library errors into the
- taxonomy with telemetry attached.
+ Telegram init-data validation and more — each translating library errors into
+ the taxonomy with telemetry attached.
- **Turnkey defaults.** The `turnkey` module ships a ready-to-use error catalog,
- helper builders and tracing instrumentation for teams that want a consistent
- baseline out of the box.
+ a heuristic classifier and conservative mappings into the canonical taxonomy
+ for teams that want a consistent baseline out of the box.
- **Typed control-flow macros.** `ensure!` and `fail!` short-circuit functions
with your domain errors without allocating or formatting on the happy path.
@@ -128,14 +131,16 @@ of redaction and metadata.
## Feature Flags
-Pick only what you need; everything is off by default.
+Pick only what you need; the default feature set is just `std`, everything
+else is opt-in.
- **Web transports:** `axum`, `actix`, `multipart`, `openapi`, `serde_json`.
- **Telemetry & observability:** `tracing`, `metrics`, `backtrace`, `colored` for
colored terminal output.
- **Async & IO integrations:** `tokio`, `reqwest`, `sqlx`, `sqlx-migrate`,
`redis`, `validator`, `config`.
-- **Messaging & bots:** `teloxide`, `telegram-webapp-sdk`.
+- **Messaging & bots:** `teloxide`, `init-data` for Telegram Mini App
+ init-data validation via `init-data-rs`.
- **Front-end tooling:** `frontend` for WASM/browser console logging.
- **gRPC:** `tonic` to emit `tonic::Status` responses.
- **Batteries included:** `turnkey` to adopt the pre-built taxonomy and helpers.
@@ -190,7 +195,7 @@ cargo bench -F benchmarks --bench error_paths
The suite emits two groups:
- `context_into_error/*` promotes a dummy source error with representative
- metadata (strings, counters, durations, IPs) through `Context::into_error` in
+ metadata (strings, counters, durations, IPs) through `ResultExt::ctx` in
both redacted and non-redacted modes.
- `problem_json_from_app_error/*` consumes the resulting `AppError` values to
build RFC 7807 payloads via `ProblemJson::from_app_error`, showing how message
@@ -468,9 +473,17 @@ generated [`masterror::Error`].
`#[provide(...)]` exposes typed context through `std::error::Request`, while
`#[app_error(...)]` records how your domain error translates into `AppError`
-and `AppCode`. The derive mirrors `thiserror`'s syntax and extends it with
-optional telemetry propagation and direct conversions into the `masterror`
-runtime types.
+and `AppCode`. The derive mirrors `thiserror`'s syntax. Note that the
+generated `From` conversions produce an `AppError` carrying only the mapped
+kind and code (plus the `Display` output as public message when the `message`
+flag is set) — the original domain error is dropped, so its sources and
+telemetry providers are not forwarded. Request telemetry from the domain
+error before converting.
+
+`request_ref`/`request_value` and the `std::error::Request` machinery require
+a nightly toolchain (`error_generic_member_access`); the crate detects
+compiler support at build time and only enables provider integration when
+available.
~~~rust
use std::error::request_ref;
@@ -502,8 +515,7 @@ let snapshot = request_ref::
(&err).expect("telemetry");
assert_eq!(snapshot.value, 42);
let app: AppError = err.into();
-let via_app = request_ref::(&app).expect("telemetry");
-assert_eq!(via_app.name, "db.query");
+assert!(matches!(app.kind, AppErrorKind::Service));
~~~
Optional telemetry only surfaces when present, so `None` does not register a
@@ -574,11 +586,10 @@ structured telemetry (`#[provide]`) and first-class conversions into
~~~rust
use masterror::{AppError, AppErrorKind, ProblemJson};
-use std::time::Duration;
let problem = ProblemJson::from_app_error(
AppError::new(AppErrorKind::Unauthorized, "Token expired")
- .with_retry_after_duration(Duration::from_secs(30))
+ .with_retry_after_secs(30)
.with_www_authenticate(r#"Bearer realm="api", error="invalid_token""#)
);
@@ -590,36 +601,19 @@ assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED");
- Environment-aware error formatting with DisplayMode
-
-The `DisplayMode` API lets you control error output formatting based on deployment
-environment without changing your error handling code. Three modes are available:
-
-- **`DisplayMode::Prod`** — Lightweight JSON output with minimal fields, optimized
- for production logs. Only includes `kind`, `code`, and `message` (if not redacted).
- Filters sensitive metadata automatically.
-
-- **`DisplayMode::Local`** — Human-readable multi-line output with full context.
- Shows error details, complete source chain, all metadata, and backtrace (if enabled).
- Best for local development and debugging.
+ Environment detection with DisplayMode
-- **`DisplayMode::Staging`** — JSON output with additional context. Includes
- `kind`, `code`, `message`, limited `source_chain`, and filtered metadata.
- Useful for staging environments where you need structured logs with more detail.
+`DisplayMode` detects the deployment environment (`Prod`, `Local` or
+`Staging`) so your code can branch on it. `DisplayMode::current()` resolves
+the mode in this order and caches the result on first access:
-**Automatic Environment Detection:**
-
-The mode is auto-detected in this order:
1. `MASTERROR_ENV` environment variable (`prod`, `local`, or `staging`)
-2. `KUBERNETES_SERVICE_HOST` presence (triggers `Prod` mode)
+2. `KUBERNETES_SERVICE_HOST` presence (selects `Prod`)
3. Build configuration (`debug_assertions` → `Local`, release → `Prod`)
-The result is cached on first access for zero-cost subsequent calls.
-
~~~rust
use masterror::DisplayMode;
-// Query the current mode (cached after first call)
let mode = DisplayMode::current();
match mode {
@@ -629,41 +623,26 @@ match mode {
}
~~~
+Note: `Display` for `AppError` does not consult `DisplayMode` yet — the
+output is identical in all modes. The only formatting branch today is the
+`colored` feature described below; mode-aware formatting is not wired up.
+
**Colored Terminal Output:**
-Enable the `colored` feature for enhanced terminal output in local mode:
+Enable the `colored` feature for enhanced terminal output. It applies
+whenever the feature is enabled, regardless of the detected mode:
~~~toml
[dependencies]
masterror = { version = "{{CRATE_VERSION}}", features = ["colored"] }
~~~
-With `colored` enabled, errors display with syntax highlighting:
-- Error kind and code in bold
-- Error messages in color
-- Source chain with indentation
-- Metadata keys highlighted
-
-~~~rust
-use masterror::{AppError, field};
-
-let error = AppError::not_found("User not found")
- .with_field(field::str("user_id", "12345"))
- .with_field(field::str("request_id", "abc-def"));
-
-// Without 'colored': plain text
-// With 'colored': color-coded output in terminals
-println!("{}", error);
-~~~
-
-**Production vs Development Output:**
-
-Without `colored` feature, errors display their `AppErrorKind` label:
+Without the `colored` feature, errors display their `AppErrorKind` label:
~~~
NotFound
~~~
-With `colored` feature, full multi-line format with context:
+With `colored`, a full multi-line format with context:
~~~
Error: NotFound
Code: NOT_FOUND
@@ -674,9 +653,6 @@ Context:
request_id: abc-def
~~~
-This separation keeps production logs clean while giving developers rich context
-during local debugging sessions.
-
@@ -702,7 +678,7 @@ Comprehensive real-world examples demonstrating masterror integration with popul
| [**custom-domain-errors**](examples/custom-domain-errors/) | Payment processing domain errors | Derive macro, error conversion, structured errors |
| [**basic-async**](examples/basic-async/) | Async error handling with tokio | Error propagation, timeout handling, Result types |
-All examples are runnable and include comprehensive tests. See the [`examples/`](examples/) directory for complete source code and documentation.
+All examples are runnable; the axum-rest-api example additionally ships integration tests. See the [`examples/`](examples/) directory for complete source code and documentation.
diff --git a/src/app_error.rs b/src/app_error.rs
index fffd18a..4b74b60 100644
--- a/src/app_error.rs
+++ b/src/app_error.rs
@@ -6,8 +6,10 @@
//!
//! [`AppError`] is a thin, framework-agnostic wrapper around a canonical
//! error taxonomy [`AppErrorKind`] plus an optional public-facing message.
-//! The `Display` for `AppError` prints only the kind, not the message, to keep
-//! logs and errors concise by default.
+//! Without the `colored` feature, the `Display` for `AppError` prints only
+//! the kind, not the message, to keep logs and errors concise by default.
+//! With `colored` enabled, `Display` renders a multi-line report (kind, code,
+//! message, source chain, metadata).
//!
//! ## Design
//!
@@ -17,7 +19,9 @@
//! secrets here.
//! - **Structured metadata:** attach typed key/value pairs for diagnostics via
//! [`Metadata`].
-//! - **No panics:** all helpers avoid `unwrap/expect`.
+//! - **No panics on user input:** helpers never panic on caller-provided data;
+//! the only internal `expect` relies on the invariant that an error chain
+//! always contains at least one element.
//! - **Transport-agnostic:** mapping to HTTP lives in `kind.rs` and
//! `convert/*`.
//!
diff --git a/src/app_error/core.rs b/src/app_error/core.rs
index 2aef940..83c02ed 100644
--- a/src/app_error/core.rs
+++ b/src/app_error/core.rs
@@ -52,12 +52,13 @@ pub mod telemetry;
/// - `CapturedBacktrace` type alias
pub mod types;
-/// Display formatting and environment-based output modes.
+/// Environment detection and alternative formatting helpers.
///
-/// Provides environment-aware error formatting with three modes:
-/// - Production: lightweight JSON, minimal fields
-/// - Local: human-readable with full context
-/// - Staging: JSON with additional context
+/// Provides [`DisplayMode`], which detects the deployment environment from
+/// `MASTERROR_ENV`, `KUBERNETES_SERVICE_HOST` or build configuration and
+/// caches the result. The `Display` implementation for `Error` does not
+/// consult this mode; it is a detection API for callers to choose their own
+/// formatting.
pub mod display;
#[cfg(all(test, feature = "backtrace"))]
diff --git a/src/app_error/core/backtrace.rs b/src/app_error/core/backtrace.rs
index 10bc4bf..ede4933 100644
--- a/src/app_error/core/backtrace.rs
+++ b/src/app_error/core/backtrace.rs
@@ -91,18 +91,8 @@ fn detect_backtrace_preference() -> bool {
///
/// This function is only available in test builds with the `backtrace` feature.
/// It clears both the global state and test override, forcing the next
-/// backtrace capture to re-read configuration.
-///
-/// # Examples
-///
-/// ```rust
-/// # #[cfg(all(test, feature = "backtrace"))]
-/// # {
-/// use masterror::app_error::core::backtrace::reset_backtrace_preference;
-///
-/// reset_backtrace_preference();
-/// # }
-/// ```
+/// backtrace capture to re-read configuration. Tests call it directly before
+/// and after overriding the preference.
#[cfg(all(test, feature = "backtrace"))]
pub fn reset_backtrace_preference() {
BACKTRACE_STATE.store(BACKTRACE_STATE_UNSET, AtomicOrdering::Release);
@@ -119,17 +109,6 @@ pub fn reset_backtrace_preference() {
///
/// * `value` - `Some(true)` to force enable, `Some(false)` to force disable,
/// `None` to clear override
-///
-/// # Examples
-///
-/// ```rust
-/// # #[cfg(all(test, feature = "backtrace"))]
-/// # {
-/// use masterror::app_error::core::backtrace::set_backtrace_preference_override;
-///
-/// set_backtrace_preference_override(Some(true));
-/// # }
-/// ```
#[cfg(all(test, feature = "backtrace"))]
pub fn set_backtrace_preference_override(value: Option
) {
test_backtrace_override::set(value);
diff --git a/src/app_error/core/display.rs b/src/app_error/core/display.rs
index cc92573..3943c2d 100644
--- a/src/app_error/core/display.rs
+++ b/src/app_error/core/display.rs
@@ -12,12 +12,15 @@ use core::{
use super::error::Error;
use crate::{FieldRedaction, FieldValue, MessageEditPolicy};
-/// Display mode for error output.
+/// Detected deployment environment.
///
-/// Controls the structure and verbosity of error messages based on
-/// the deployment environment. The mode is determined by the
-/// `MASTERROR_ENV` environment variable or auto-detected based on
-/// build configuration and runtime environment.
+/// [`DisplayMode::current`] identifies the environment the process runs in,
+/// based on the `MASTERROR_ENV` environment variable, Kubernetes detection
+/// (`KUBERNETES_SERVICE_HOST`) or build configuration, and caches the result.
+///
+/// Note: the `Display` implementation for [`struct@crate::Error`] does not
+/// currently switch its output based on this mode; the mode is a detection
+/// API that callers can consult to choose their own formatting.
///
/// # Examples
///
@@ -26,62 +29,33 @@ use crate::{FieldRedaction, FieldValue, MessageEditPolicy};
///
/// let mode = DisplayMode::current();
/// match mode {
-/// DisplayMode::Prod => println!("Production mode: JSON output"),
-/// DisplayMode::Local => println!("Local mode: Human-readable output"),
-/// DisplayMode::Staging => println!("Staging mode: JSON with context")
+/// DisplayMode::Prod => println!("Production environment"),
+/// DisplayMode::Local => println!("Local development environment"),
+/// DisplayMode::Staging => println!("Staging environment")
/// }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisplayMode {
- /// Production mode: lightweight JSON, minimal fields, no sensitive data.
- ///
- /// Output includes only: `kind`, `code`, `message` (if not redacted).
- /// Metadata is filtered to exclude sensitive fields.
- /// Source chain and backtrace are excluded.
- ///
- /// # Example Output
+ /// Production environment.
///
- /// ```json
- /// {"kind":"NotFound","code":"NOT_FOUND","message":"User not found"}
- /// ```
+ /// Selected when `MASTERROR_ENV` is `prod`/`production`, when
+ /// `KUBERNETES_SERVICE_HOST` is set, or for release builds by default.
Prod = 0,
- /// Development mode: human-readable, full context.
- ///
- /// Output includes: error details, full source chain, complete metadata,
- /// and backtrace (if enabled). Supports colored output when the `colored`
- /// feature is enabled and output is a TTY.
+ /// Local development environment.
///
- /// # Example Output
- ///
- /// ```text
- /// Error: NotFound
- /// Code: NOT_FOUND
- /// Message: User not found
- ///
- /// Caused by: database query failed
- /// Caused by: connection timeout
- ///
- /// Context:
- /// user_id: 12345
- /// ```
+ /// Selected when `MASTERROR_ENV` is `local`/`dev`/`development`, or for
+ /// debug builds by default.
Local = 1,
- /// Staging mode: JSON with additional context.
+ /// Staging environment.
///
- /// Output includes: `kind`, `code`, `message`, limited `source_chain`,
- /// and filtered metadata. No backtrace.
- ///
- /// # Example Output
- ///
- /// ```json
- /// {"kind":"NotFound","code":"NOT_FOUND","message":"User not found","source_chain":["database error"],"metadata":{"user_id":12345}}
- /// ```
+ /// Selected when `MASTERROR_ENV` is `staging`/`stage`.
Staging = 2
}
impl DisplayMode {
- /// Returns the current display mode based on environment configuration.
+ /// Returns the detected environment based on configuration.
///
/// The mode is determined by checking (in order):
/// 1. `MASTERROR_ENV` environment variable (`prod`, `local`, or `staging`)
@@ -152,21 +126,15 @@ impl DisplayMode {
#[allow(dead_code)]
impl Error {
- /// Formats error in production mode (compact JSON).
+ /// Formats the error as compact JSON (`kind`, `code`, optional `message`,
+ /// public metadata).
+ ///
+ /// Not wired into the `Display` implementation; callers that want this
+ /// layout must invoke it explicitly.
///
/// # Arguments
///
/// * `f` - Formatter to write output to
- ///
- /// # Examples
- ///
- /// ```
- /// use masterror::AppError;
- ///
- /// let error = AppError::not_found("User not found");
- /// let output = format!("{}", error);
- /// // In prod mode: {"kind":"NotFound","code":"NOT_FOUND","message":"User not found"}
- /// ```
#[cfg(not(test))]
pub(crate) fn fmt_prod(&self, f: &mut Formatter<'_>) -> FmtResult {
self.fmt_prod_impl(f)
@@ -212,21 +180,15 @@ impl Error {
write!(f, "}}")
}
- /// Formats error in local/development mode (human-readable).
+ /// Formats the error as a multi-line human-readable report (kind, code,
+ /// message, source chain, metadata).
+ ///
+ /// Not wired into the `Display` implementation; callers that want this
+ /// layout must invoke it explicitly.
///
/// # Arguments
///
/// * `f` - Formatter to write output to
- ///
- /// # Examples
- ///
- /// ```
- /// use masterror::AppError;
- ///
- /// let error = AppError::internal("Database error");
- /// let output = format!("{}", error);
- /// // In local mode: multi-line human-readable format with full context
- /// ```
#[cfg(not(test))]
pub(crate) fn fmt_local(&self, f: &mut Formatter<'_>) -> FmtResult {
self.fmt_local_impl(f)
@@ -307,21 +269,15 @@ impl Error {
}
}
- /// Formats error in staging mode (JSON with context).
+ /// Formats the error as JSON with additional context (`source_chain` and
+ /// public metadata).
+ ///
+ /// Not wired into the `Display` implementation; callers that want this
+ /// layout must invoke it explicitly.
///
/// # Arguments
///
/// * `f` - Formatter to write output to
- ///
- /// # Examples
- ///
- /// ```
- /// use masterror::AppError;
- ///
- /// let error = AppError::service("Service unavailable");
- /// let output = format!("{}", error);
- /// // In staging mode: JSON with source_chain and metadata
- /// ```
#[cfg(not(test))]
pub(crate) fn fmt_staging(&self, f: &mut Formatter<'_>) -> FmtResult {
self.fmt_staging_impl(f)
diff --git a/src/app_error/core/introspection.rs b/src/app_error/core/introspection.rs
index f27c6d8..da6ccec 100644
--- a/src/app_error/core/introspection.rs
+++ b/src/app_error/core/introspection.rs
@@ -185,10 +185,10 @@ impl Error {
.expect("chain always has at least one error")
}
- /// Attempts to downcast the error source to a concrete type.
+ /// Check whether the attached source error is of a concrete type.
///
- /// Returns `true` if the error source is of type `E`, `false` otherwise.
- /// This only checks the immediate source, not the entire chain.
+ /// Returns `true` if the immediate source (not this error itself, and not
+ /// the entire chain) is of type `E`, `false` otherwise.
///
/// # Examples
///
@@ -215,7 +215,7 @@ impl Error {
self.source_ref().is_some_and(|source| source.is::())
}
- /// Attempt to downcast the error source to a concrete type by value.
+ /// Attempt to take ownership of the source error as a concrete type.
///
/// **Note:** This method is currently a stub and always returns
/// `Err(Self)`.
@@ -243,10 +243,11 @@ impl Error {
Err(self)
}
- /// Attempt to downcast the error to a concrete type by immutable
- /// reference.
+ /// Attempt to downcast the attached source error to a concrete type by
+ /// immutable reference.
///
- /// Returns `Some(&E)` if this error is of type `E`, `None` otherwise.
+ /// Returns `Some(&E)` if the immediate source is of type `E`, `None`
+ /// otherwise (including when no source is attached).
///
/// # Examples
///
@@ -272,9 +273,12 @@ impl Error {
self.source_ref()?.downcast_ref::()
}
- /// Attempt to downcast the error to a concrete type by mutable reference.
+ /// Attempt to downcast the attached source error to a concrete type by
+ /// mutable reference.
///
- /// Returns `Some(&mut E)` if this error is of type `E`, `None` otherwise.
+ /// **Note:** This method is currently a stub and always returns `None`.
+ ///
+ /// Use [`downcast_ref`](Self::downcast_ref) for inspecting error sources.
///
/// # Examples
///
@@ -287,9 +291,7 @@ impl Error {
/// let io_err = IoError::other("disk offline");
/// let mut err = AppError::internal("boom").with_context(io_err);
///
- /// if let Some(_io) = err.downcast_mut::() {
- /// // Can modify the IoError if needed
- /// }
+ /// assert!(err.downcast_mut::().is_none());
/// # }
/// ```
#[must_use]
diff --git a/src/code/app_code.rs b/src/code/app_code.rs
index d46b585..be89529 100644
--- a/src/code/app_code.rs
+++ b/src/code/app_code.rs
@@ -149,8 +149,9 @@ impl AppCode {
///
/// # Errors
///
- /// Returns [`ParseAppCodeError`] when the string is empty or contains
- /// characters outside of `A-Z`, `0-9`, and `_`.
+ /// Returns [`ParseAppCodeError`] when the string is empty, contains
+ /// characters outside of `A-Z`, `0-9`, and `_`, or has leading, trailing
+ /// or consecutive underscores.
///
/// # Examples
/// ```
diff --git a/src/convert.rs b/src/convert.rs
index 50de21f..d780602 100644
--- a/src/convert.rs
+++ b/src/convert.rs
@@ -9,10 +9,11 @@
//! the crate’s [`AppError`]. It also conditionally enables transport adapters
//! (Axum/Actix) to turn [`AppError`] into HTTP responses.
//!
-//! ## Always-on mappings
+//! ## Base mappings
//!
-//! - [`std::io::Error`] → `AppErrorKind::Internal` Infrastructure-level
-//! failure; message preserved for logs only.
+//! - [`std::io::Error`] → `AppErrorKind::Internal` (requires the default `std`
+//! feature) Infrastructure-level failure; the error text becomes the public
+//! message.
//! - [`String`] → `AppErrorKind::BadRequest` Lightweight validation helper
//! when you don’t pull in `validator`.
//!
@@ -24,12 +25,15 @@
//! - `axum` + `multipart`: Axum multipart parsing errors
//! - `actix`: Actix `ResponseError` integration (not a mapping, but transport)
//! - `config`: configuration loader errors
+//! - `init-data`: Telegram Mini Apps init-data validation errors
//! - `redis`: Redis client errors
//! - `reqwest`: HTTP client errors
-//! - `serde_json`: JSON conversion helpers (if you attach JSON details
-//! manually)
-//! - `sqlx`: database driver errors
+//! - `serde_json`: `serde_json::Error` classification into
+//! `Serialization`/`Deserialization`
+//! - `sqlx`: database driver errors (`sqlx-core`)
+//! - `sqlx-migrate`: `sqlx::migrate::MigrateError` mapping
//! - `tokio`: timeouts from `tokio::time::error::Elapsed`
+//! - `tonic`: conversions between [`struct@crate::Error`] and `tonic::Status`
//! - `teloxide`: Telegram request errors
//! - `validator`: input DTO validation errors
//!
@@ -144,9 +148,10 @@ pub use self::tonic::StatusConversionError;
/// Map `std::io::Error` to an internal application error.
///
-/// Rationale: I/O failures are infrastructure-level and should not leak
-/// driver-specific details to clients. The message is preserved for
-/// observability, but the public-facing kind is always `Internal`.
+/// Rationale: I/O failures are infrastructure-level, so the public-facing
+/// kind is always `Internal`. The error text becomes the public message, so
+/// avoid embedding sensitive details in I/O error messages that cross this
+/// boundary.
///
/// ```rust
/// use std::io::{self, ErrorKind};
diff --git a/src/convert/actix.rs b/src/convert/actix.rs
index 677080f..28d28c1 100644
--- a/src/convert/actix.rs
+++ b/src/convert/actix.rs
@@ -10,27 +10,24 @@
//! - Implements `actix_web::ResponseError` for [`AppError`].
//! - This lets you `return AppResult<_>` from Actix handlers.
//! - On error, Actix automatically builds an `HttpResponse` with the right
-//! status code and RFC7807 JSON body (when the `serde_json` feature is
-//! enabled).
+//! status code and an RFC7807 JSON body.
//! - Provides stable mapping from [`AppErrorKind`] to
//! `actix_web::http::StatusCode`.
//! - Ensures that only safe, public-facing fields are returned to the client
-//! (`type`, `title`, `status`, `detail?`, `metadata?`).
+//! (`type`, `title`, `status`, `code`, `detail?`, `metadata?`, `grpc?`).
//!
//! ## Wire payload
//!
-//! When the `serde_json` feature is enabled, the body is [`ProblemJson`] with:
+//! The body is always [`ProblemJson`] with:
//! - `type`: canonical URI describing the problem class
//! - `title`: short summary derived from [`AppErrorKind`]
//! - `status`: numeric HTTP status (e.g. 404, 422, 500)
+//! - `code`: stable machine-readable [`AppCode`](crate::AppCode)
//! - `detail?`: public message (redacted when the error is private)
//! - `metadata?`: sanitized structured fields carried from
//! [`Metadata`](crate::Metadata)
//! - `grpc?`: optional gRPC mapping for multi-protocol clients
//!
-//! Without `serde_json`, Actix still returns a response with the correct status
-//! but with an empty body.
-//!
//! ## Example
//!
//! ```rust,ignore
@@ -70,10 +67,9 @@
//! - Do not duplicate this `ResponseError` implementation elsewhere.
//! - Internal error sources (`std::error::Error` chain) are logged only; they
//! are never leaked to the HTTP response.
-//! - You typically want both `actix` and `serde_json` features enabled for
-//! proper JSON payloads.
//!
-//! See also: Axum integration in [`convert::axum`].
+//! See also: the Axum integration provided by the `axum` feature
+//! (`convert/axum.rs`).
#[cfg(feature = "actix")]
use actix_web::{HttpResponse, ResponseError, http::StatusCode as ActixStatus};
diff --git a/src/convert/init_data.rs b/src/convert/init_data.rs
index a7db318..51bb15b 100644
--- a/src/convert/init_data.rs
+++ b/src/convert/init_data.rs
@@ -8,8 +8,10 @@
//!
//! ## Mapping
//!
-//! All [`InitDataError`] variants are mapped to `AppErrorKind::TelegramAuth`
-//! and the original error text is preserved in the message.
+//! All [`InitDataError`] variants are mapped to `AppErrorKind::TelegramAuth`.
+//! The public message is left unset; the failure reason and details are
+//! captured as `telegram_init_data.*` metadata fields and the original error
+//! is retained in the source chain.
//!
//! ## Rationale
//!
@@ -20,10 +22,9 @@
//!
//! ## Example
//!
-//! '''rust
+//! ```rust
//! # #[cfg(feature = "init-data")]
//! # {
-//! '''rust,ignore
//! use init_data_rs::InitDataError;
//! use masterror::{AppErrorKind, Error};
//!
@@ -34,7 +35,7 @@
//! let e = convert(InitDataError::HashMissing);
//! assert!(matches!(e.kind, AppErrorKind::TelegramAuth));
//! # }
-//! '''
+//! ```
#[cfg(feature = "init-data")]
use init_data_rs::InitDataError;
diff --git a/src/convert/multipart.rs b/src/convert/multipart.rs
index 251e668..d8f243e 100644
--- a/src/convert/multipart.rs
+++ b/src/convert/multipart.rs
@@ -3,7 +3,10 @@
// SPDX-License-Identifier: MIT
//! Maps [`MultipartError`] into [`Error`] with
-//! [`AppErrorKind::BadRequest`], preserving the original message.
+//! [`AppErrorKind::BadRequest`]. The public message is left unset; the
+//! multipart body text and HTTP status are captured as metadata fields
+//! (`multipart.reason`, `http.*`) and the original error is retained in the
+//! source chain.
//!
//! Intended for Axum multipart form parsing so that client mistakes are
//! surfaced as bad requests.
diff --git a/src/convert/redis.rs b/src/convert/redis.rs
index bce2e3c..b771293 100644
--- a/src/convert/redis.rs
+++ b/src/convert/redis.rs
@@ -21,19 +21,19 @@
//!
//! ## Example
//!
-//! ```rust,ignore
+//! An IO-level failure is promoted from `Cache` to `DependencyUnavailable`:
+//!
+//! ```rust
+//! # #[cfg(feature = "redis")]
+//! # {
//! use masterror::{AppErrorKind, Error};
//! use redis::RedisError;
//!
-//! fn handle_cache_error(e: RedisError) -> Error {
-//! e.into()
-//! }
-//!
-//! // In production code, this would come from a Redis client operation
-//! let dummy = RedisError::from((redis::ErrorKind::Io, "connection lost"));
-//! let app_err = handle_cache_error(dummy);
+//! let err = RedisError::from((redis::ErrorKind::Io, "connection lost"));
+//! let app_err: Error = err.into();
//!
-//! assert!(matches!(app_err.kind, AppErrorKind::Cache));
+//! assert!(matches!(app_err.kind, AppErrorKind::DependencyUnavailable));
+//! # }
//! ```
#[cfg(feature = "redis")]
@@ -42,10 +42,14 @@ use redis::{ErrorKind, RedisError, RetryMethod, ServerErrorKind};
#[cfg(feature = "redis")]
use crate::{AppErrorKind, Context, Error, field};
-/// Map any [`redis::RedisError`] into an [`crate::AppError`] with kind `Cache`.
+/// Map a [`redis::RedisError`] into an [`crate::AppError`].
///
-/// Rationale: Redis is treated as a backend cache dependency.
-/// Detailed driver errors are kept in the message for diagnostics.
+/// Rationale: Redis is treated as a backend cache dependency, so the default
+/// kind is `Cache`. Timeouts are promoted to `Timeout`; IO, connection,
+/// cluster and busy-loading failures are promoted to `DependencyUnavailable`.
+/// The public message is left unset; driver details are captured as structured
+/// metadata fields (`redis.*`) and the original error is retained in the
+/// source chain for diagnostics.
#[cfg(feature = "redis")]
#[cfg_attr(docsrs, doc(cfg(feature = "redis")))]
impl From for Error {
diff --git a/src/convert/reqwest.rs b/src/convert/reqwest.rs
index 7b73c31..2f433a5 100644
--- a/src/convert/reqwest.rs
+++ b/src/convert/reqwest.rs
@@ -62,7 +62,8 @@ use crate::{AppErrorKind, Context, Error, FieldRedaction, field};
///
/// - Timeout → `Timeout`
/// - Connect or request build error → `Network`
-/// - Upstream returned HTTP error status → `ExternalApi`
+/// - Upstream HTTP error status: `429` → `RateLimited`, `408` → `Timeout`,
+/// `5xx` → `DependencyUnavailable`, others → `ExternalApi`
/// - Fallback for other cases → `ExternalApi`
///
/// # Example
diff --git a/src/convert/serde_json.rs b/src/convert/serde_json.rs
index c7a0ed6..6531cc8 100644
--- a/src/convert/serde_json.rs
+++ b/src/convert/serde_json.rs
@@ -10,8 +10,9 @@
//!
//! Errors are classified using [`serde_json::Error::classify`]. I/O failures
//! are mapped to [`AppErrorKind::Serialization`]; syntax, data and EOF errors
-//! map to [`AppErrorKind::Deserialization`]. The original error string is
-//! preserved in `message` for observability.
+//! map to [`AppErrorKind::Deserialization`]. The public message is left unset;
+//! category and position details are captured as `serde_json.*` metadata
+//! fields and the original error is retained in the source chain.
//!
//! ## Rationale
//!
@@ -46,8 +47,8 @@ use crate::{AppErrorKind, Context, Error, field};
/// Map a [`serde_json::Error`] into an [`crate::AppError`].
///
/// Errors are classified to `Serialization` or `Deserialization` using
-/// [`serde_json::Error::classify`]. The original error string is preserved for
-/// logs and optional JSON payloads.
+/// [`serde_json::Error::classify`]. Category and position details go into
+/// metadata; the original error stays available via the source chain.
#[cfg(feature = "serde_json")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
impl From for Error {
diff --git a/src/convert/teloxide.rs b/src/convert/teloxide.rs
index 291aef1..a8cdf1b 100644
--- a/src/convert/teloxide.rs
+++ b/src/convert/teloxide.rs
@@ -16,7 +16,9 @@
//! - [`RequestError::InvalidJson`] → `AppErrorKind::Deserialization`
//! - [`RequestError::Io`] → `AppErrorKind::Internal`
//!
-//! The original error string is preserved in the `message` for observability.
+//! The public message is left unset; error details are captured as
+//! `telegram.*` metadata fields and the original error is retained in the
+//! source chain for observability.
//!
//! ## Example
//!
diff --git a/src/convert/tokio.rs b/src/convert/tokio.rs
index 03713cb..865b76c 100644
--- a/src/convert/tokio.rs
+++ b/src/convert/tokio.rs
@@ -8,8 +8,10 @@
//!
//! ## Mapping
//!
-//! All elapsed-time errors are mapped to `AppErrorKind::Timeout` with a fixed,
-//! public-facing message `"Operation timed out"`.
+//! All elapsed-time errors are mapped to `AppErrorKind::Timeout`. The
+//! conversion sets no public message, so clients see the kind's fallback
+//! title; a `timeout.source` metadata field records the timeout origin and
+//! the original error is retained in the source chain.
//!
//! ## Rationale
//!
@@ -45,7 +47,8 @@ use crate::{AppErrorKind, Context, Error, field};
/// Map a [`tokio::time::error::Elapsed`] into an [`crate::AppError`] with kind
/// `Timeout`.
///
-/// Message is fixed to avoid leaking timing specifics to the client.
+/// No public message is set, which avoids leaking timing specifics to the
+/// client; a `timeout.source` metadata field is attached instead.
///
/// # Example
///
diff --git a/src/lib.rs b/src/lib.rs
index e086186..ec0a9f2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -46,7 +46,7 @@
//!
//! # Minimum Supported Rust Version (MSRV)
//!
-//! MSRV is **1.90**. New minor releases may increase MSRV with a changelog
+//! MSRV is **1.96**. New minor releases may increase MSRV with a changelog
//! note, but never in a patch release.
//!
//! # Feature flags
@@ -60,21 +60,29 @@
//! - `tonic` — converts [`struct@Error`] into `tonic::Status` with sanitized
//! metadata
//! - `openapi` — derives an OpenAPI schema for [`ErrorResponse`] (via `utoipa`)
-//! - `sqlx` — `From` mapping
+//! - `sqlx` — `From` mapping (pulls only `sqlx-core`)
+//! - `sqlx-migrate` — `From` mapping (pulls full
+//! `sqlx`)
//! - `redis` — `From` mapping
//! - `validator` — `From` mapping
//! - `config` — `From` mapping
//! - `tokio` — `From` mapping
//! - `reqwest` — `From` mapping
//! - `teloxide` — `From` mapping
-//! - `telegram-webapp-sdk` —
-//! `From`
-//! mapping
+//! - `init-data` — `From` mapping for Telegram
+//! Mini Apps init-data validation
//! - `frontend` — convert errors into `wasm_bindgen::JsValue` and emit
//! `console.error` logs in WASM/browser contexts
//! - `serde_json` — support for structured JSON details in [`ErrorResponse`]
-//! and [`ProblemJson`]; also pulled transitively by `axum`
-//! - `multipart` — compatibility flag for Axum multipart
+//! and [`ProblemJson`] plus a `From` mapping. Note:
+//! `axum`/`actix` enable the `serde_json` *dependency* for their own bodies,
+//! but not this feature — enable it explicitly to get the JSON-gated APIs
+//! - `multipart` — `From` mapping to
+//! `BadRequest` (implies `axum`)
+//! - `colored` — colored, multi-line `Display` output for [`struct@Error`]
+//! - `tracing` — emit structured `tracing` events when errors are constructed
+//! - `metrics` — increment an `error_total{code,category}` counter per error
+//! - `backtrace` — capture backtraces (controlled by `RUST_BACKTRACE`)
//! - `turnkey` — domain taxonomy and conversions for Turnkey errors, exposed in
//! the `turnkey` module
//!
diff --git a/src/response.rs b/src/response.rs
index 86979a5..4308bdb 100644
--- a/src/response.rs
+++ b/src/response.rs
@@ -12,7 +12,7 @@
//!
//! - canonical problem `type` URIs derived from [`AppCode`]
//! - a `title` computed from [`AppErrorKind`]
-//! - the stable machine code plus optional gRPC mapping (`grpc.code`,
+//! - the stable machine code plus optional gRPC mapping (`grpc.name`,
//! `grpc.value`)
//! - retry/authentication hints surfaced via the `Retry-After` and
//! `WWW-Authenticate` headers
diff --git a/src/response/actix_impl.rs b/src/response/actix_impl.rs
index 8fc6f8d..303bdbd 100644
--- a/src/response/actix_impl.rs
+++ b/src/response/actix_impl.rs
@@ -9,7 +9,10 @@
//! - Serializes the response as RFC7807 `application/problem+json`.
//! - Adds `Retry-After` when retry advice is present.
//! - Adds `WWW-Authenticate` when an authentication challenge is provided.
-//! - Redacts message and metadata when the error is marked private.
+//! - Redaction itself happens earlier, in
+//! [`ProblemJson::from_app_error`](crate::ProblemJson::from_app_error), which
+//! applies the error's message and metadata redaction policies before the
+//! payload reaches this adapter.
use actix_web::{
HttpRequest, HttpResponse, Responder,
diff --git a/src/response/axum_impl.rs b/src/response/axum_impl.rs
index b939c22..b3cbba3 100644
--- a/src/response/axum_impl.rs
+++ b/src/response/axum_impl.rs
@@ -10,7 +10,10 @@
//! status.
//! - Adds `Retry-After` if retry advice is present.
//! - Adds `WWW-Authenticate` if an authentication challenge is present.
-//! - Redacts the message and metadata when the error is marked as private.
+//! - Redaction itself happens earlier, in
+//! [`ProblemJson::from_app_error`](crate::ProblemJson::from_app_error), which
+//! applies the error's message and metadata redaction policies before the
+//! payload reaches this adapter.
use axum::{
Json,
diff --git a/src/result_ext.rs b/src/result_ext.rs
index afe9b65..ccc6777 100644
--- a/src/result_ext.rs
+++ b/src/result_ext.rs
@@ -40,8 +40,12 @@ pub trait ResultExt {
/// Wrap the error with a simple context message.
///
- /// This is a convenience method equivalent to anyhow's `.context()`.
- /// For more control, use [`ctx`](ResultExt::ctx).
+ /// This is a convenience method in the spirit of anyhow's `.context()`,
+ /// with one difference: when the source is not already a masterror
+ /// [`Error`], the result is classified as `Internal` (whereas anyhow
+ /// keeps errors unclassified). If the source is already an [`Error`],
+ /// its kind, code and metadata are preserved and only the message is
+ /// replaced. For control over the category, use [`ctx`](ResultExt::ctx).
///
/// # Examples
///