Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.20.6] - 2025-10-06

### Fixed
- Restored compilation on Rust 1.90+ by aliasing the infallible gRPC
conversion error to `core::convert::Infallible` and re-exporting it without
exposing the private `convert::tonic` module.

## [0.20.5] - 2025-10-05

### Changed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "masterror"
version = "0.20.5"
version = "0.20.6"
rust-version = "1.90"
edition = "2024"
license = "MIT OR Apache-2.0"
Expand Down
142 changes: 3 additions & 139 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ The build script keeps the full feature snippet below in sync with

~~~toml
[dependencies]
masterror = { version = "0.20.5", default-features = false }
masterror = { version = "0.20.6", default-features = false }
# or with features:
# masterror = { version = "0.20.5", features = [
# masterror = { version = "0.20.6", features = [
# "axum", "actix", "openapi", "serde_json",
# "tracing", "metrics", "backtrace", "sqlx",
# "sqlx-migrate", "reqwest", "redis", "validator",
Expand All @@ -85,7 +85,6 @@ masterror = { version = "0.20.5", default-features = false }
---

<details>

<summary><b>Quick start</b></summary>

Create an error:
Expand Down Expand Up @@ -378,143 +377,8 @@ assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED");
~~~

</details>
<details>
<summary><b>Web framework integrations</b></summary>

<details>
<summary>Axum</summary>

~~~rust
// features = ["axum", "serde_json"]
...
assert!(payload.is_object());

#[cfg(target_arch = "wasm32")]
{
if let Err(console_err) = err.log_to_browser_console() {
eprintln!(
"failed to log to browser console: {:?}",
console_err.context()
);
}
}

Ok(())
}
~~~

- On non-WASM targets `log_to_browser_console` returns
`BrowserConsoleError::UnsupportedTarget`.
- `BrowserConsoleError::context()` exposes optional browser diagnostics for
logging/telemetry when console logging fails.

</details>

</details>

<details>
<summary><b>Feature flags</b></summary>

- `axum` — IntoResponse integration with structured JSON bodies
- `actix` — Actix Web ResponseError and Responder implementations
- `openapi` — Generate utoipa OpenAPI schema for ErrorResponse
- `serde_json` — Attach structured JSON details to AppError
- `tracing` — Emit structured tracing events when errors are constructed
- `metrics` — Increment `error_total{code,category}` counter for each AppError
- `backtrace` — Capture lazy `Backtrace` snapshots when telemetry is flushed
- `sqlx` — Classify sqlx_core::Error variants into AppError kinds
- `sqlx-migrate` — Map sqlx::migrate::MigrateError into AppError (Database)
- `reqwest` — Classify reqwest::Error as timeout/network/external API
- `redis` — Map redis::RedisError into cache-aware AppError
- `validator` — Convert validator::ValidationErrors into validation failures
- `config` — Propagate config::ConfigError as configuration issues
- `tokio` — Classify tokio::time::error::Elapsed as timeout
- `multipart` — Handle axum multipart extraction errors
- `teloxide` — Convert teloxide_core::RequestError into domain errors
- `telegram-webapp-sdk` — Surface Telegram WebApp validation failures
- `tonic` — Convert AppError into tonic::Status with redaction
- `frontend` — Log to the browser console and convert to JsValue on WASM
- `turnkey` — Ship Turnkey-specific error taxonomy and conversions

</details>

<details>
<summary><b>Conversions</b></summary>

- `std::io::Error` → Internal
- `String` → BadRequest
- `sqlx::Error` → NotFound/Database
- `redis::RedisError` → Cache
- `reqwest::Error` → Timeout/Network/ExternalApi
- `axum::extract::multipart::MultipartError` → BadRequest
- `validator::ValidationErrors` → Validation
- `config::ConfigError` → Config
- `tokio::time::error::Elapsed` → Timeout
- `teloxide_core::RequestError` → RateLimited/Network/ExternalApi/Deserialization/Internal
- `telegram_webapp_sdk::utils::validate_init_data::ValidationError` → TelegramAuth

</details>

<details>
<summary><b>Typical setups</b></summary>

Minimal core:

~~~toml
masterror = { version = "0.20.5", default-features = false }
~~~

API (Axum + JSON + deps):

~~~toml
masterror = { version = "0.20.5", features = [
"axum", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
~~~

API (Actix + JSON + deps):

~~~toml
masterror = { version = "0.20.5", features = [
"actix", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
~~~

</details>

<details>
<summary><b>Turnkey</b></summary>

~~~rust
// features = ["turnkey"]
use masterror::turnkey::{classify_turnkey_error, TurnkeyError, TurnkeyErrorKind};
use masterror::{AppError, AppErrorKind};

// Classify a raw SDK/provider error
let kind = classify_turnkey_error("429 Too Many Requests");
assert!(matches!(kind, TurnkeyErrorKind::RateLimited));

// Wrap into AppError
let e = TurnkeyError::new(TurnkeyErrorKind::RateLimited, "throttled upstream");
let app: AppError = e.into();
assert_eq!(app.kind, AppErrorKind::RateLimited);
~~~

</details>

<details>
<summary><b>Migration 0.2 → 0.3</b></summary>

- Use `ErrorResponse::new(status, AppCode::..., "msg")` instead of legacy
- New helpers: `.with_retry_after_secs`, `.with_retry_after_duration`, `.with_www_authenticate`
- `ErrorResponse::new_legacy` is temporary shim

</details>

<details>
<summary><b>Versioning & MSRV</b></summary>
### Further resources

- Explore the [error-handling wiki](docs/wiki/index.md) for step-by-step guides,
comparisons with `thiserror`/`anyhow`, and troubleshooting recipes.
Expand Down
3 changes: 3 additions & 0 deletions src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ mod telegram_webapp_sdk;
#[cfg_attr(docsrs, doc(cfg(feature = "tonic")))]
mod tonic;

#[cfg(feature = "tonic")]
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
Expand Down
45 changes: 13 additions & 32 deletions src/convert/tonic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@
//! ## Example
//!
//! ```rust,ignore
//! use masterror::{AppError, AppErrorKind};
//! use masterror::AppError;
//!
//! let status = tonic::Status::from(AppError::not_found("missing"));
//! assert_eq!(status.code(), tonic::Code::NotFound);
//! ```

use std::{borrow::Cow, fmt};
use core::convert::Infallible;
use std::borrow::Cow;

use tonic::{
Code, Status,
Expand All @@ -32,52 +33,32 @@ use crate::{
mapping_for_code
};

/// Error returned when converting [`Error`] into [`Status`] fails.
/// Error alias retained for backwards compatibility with 0.20 conversions.
///
/// This type is never constructed in practice because the conversion is
/// guaranteed to succeed. It exists solely to preserve the `TryFrom` API in
/// addition to the infallible [`From`] conversion.
/// Since Rust 1.90 the standard library implements [`TryFrom`] for every
/// [`Into`] conversion with [`core::convert::Infallible`] as the error type.
/// Tonic conversions are therefore guaranteed to succeed, and this alias keeps
/// the historic [`StatusConversionError`] name available for downstream APIs.
///
/// # Examples
/// ```rust,ignore
/// use masterror::{AppError, StatusConversionError};
/// use tonic::{Code, Status};
///
/// fn convert() -> Result<Status, StatusConversionError> {
/// Status::try_from(AppError::not_found("missing"))
/// }
///
/// # fn main() -> Result<(), StatusConversionError> {
/// let status = convert()?;
/// let status: Result<Status, StatusConversionError> = Status::try_from(
/// AppError::not_found("missing")
/// );
/// let status = status.expect("conversion cannot fail");
/// assert_eq!(status.code(), Code::NotFound);
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StatusConversionError;

impl fmt::Display for StatusConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("conversion to tonic::Status cannot fail")
}
}

impl std::error::Error for StatusConversionError {}
pub type StatusConversionError = Infallible;

impl From<Error> for Status {
fn from(error: Error) -> Self {
status_from_error(&error)
}
}

impl TryFrom<Error> for Status {
type Error = StatusConversionError;

fn try_from(error: Error) -> Result<Self, Self::Error> {
Ok(Status::from(error))
}
}

fn status_from_error(error: &Error) -> Status {
error.emit_telemetry();

Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,4 +366,4 @@ pub use result_ext::ResultExt;

#[cfg(feature = "tonic")]
#[cfg_attr(docsrs, doc(cfg(feature = "tonic")))]
pub use crate::convert::tonic::StatusConversionError;
pub use crate::convert::StatusConversionError;
Loading