Skip to content

Structured, traceable, retry-aware HTTP error responses for Rust APIs. Features anyhow and Axum integration with a framework-agnostic core.

License

Notifications You must be signed in to change notification settings

blackwell-systems/error-envelope

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

49 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

error-envelope

Blackwell Systemsβ„’ Crates.io Docs.rs CI License: MIT Sponsor

Structured, traceable, retry-aware HTTP error responses for Rust APIs. Features anyhow and Axum integration with a framework-agnostic core.

Overview

  • anyhow integration: Automatic conversion from anyhow::Error into error_envelope::Error at the HTTP boundary
  • thiserror mapping: Implement From for explicit HTTP semantics (no accidental 500s)
  • Axum support: Implements IntoResponse for seamless API error handling
  • Consistent error format: One predictable JSON structure for all HTTP errors
  • Typed error codes: 18 standard codes as a type-safe enum
  • Traceability: Built-in support for trace IDs and retry hints
  • Framework-agnostic core: Works standalone; integrations are opt-in via features

The stack: anyhow for propagation β†’ error-envelope at the HTTP boundary β†’ Axum via axum-support

use axum::{extract::Path, Json};
use error_envelope::{Error, validation};
use std::collections::HashMap;

#[derive(serde::Deserialize)]
struct CreateUser { email: String, age: u8 }

#[derive(serde::Serialize)]
struct User { id: String, email: String }

// Automatic conversion from anyhow:
async fn get_user(Path(id): Path<String>) -> Result<Json<User>, Error> {
    // db::find_user is your app code (returns anyhow::Result<User>)
    let user = db::find_user(&id).await?; // anyhow error converts automatically
    Ok(Json(user))
}

// Structured validation errors:
async fn create_user(Json(data): Json<CreateUser>) -> Result<Json<User>, Error> {
    let mut errors = HashMap::new();
    
    if !data.email.contains('@') {
        errors.insert("email".to_string(), "must be a valid email".to_string());
    }
    if data.age < 18 {
        errors.insert("age".to_string(), "must be 18 or older".to_string());
    }
    
    if !errors.is_empty() {
        return Err(validation(errors).with_trace_id("abc-123"));
    }
    
    Ok(Json(User { id: "123".to_string(), email: data.email }))
}

// On validation error, returns HTTP 400:
// {
//   "code": "VALIDATION_FAILED",
//   "message": "Invalid input",
//   "details": {
//     "fields": {
//       "email": "must be a valid email",
//       "age": "must be 18 or older"
//     }
//   },
//   "trace_id": "abc-123",
//   "retryable": false
// }

Table of Contents

Why error-envelope

APIs need a formal contract for errors. Without one, clients can't predict error structure:

{"error": "bad request"}

String field, no structure.

{"message": "invalid", "code": 400}

Different field names, ad-hoc.

{"errors": [{"field": "email"}]}

Array structure, incompatible.

Every endpoint becomes a special case. error-envelope establishes a predictable contract: same structure, same fields, every time.

Installation

[dependencies]
error-envelope = "0.3"

With optional features:

[dependencies]
error-envelope = { version = "0.3", features = ["axum-support", "anyhow-support"] }

You can enable either or both features depending on your use case.

πŸ“š Full API documentation: docs.rs/error-envelope

Crate Features

Feature Description
default Core error envelope with no framework dependencies
axum-support Adds IntoResponse implementation for Axum framework integration
anyhow-support Enables From<anyhow::Error> conversion for seamless interop with anyhow

Quick Start

use error_envelope::Error;
use std::time::Duration;

// Create errors with builder pattern:
let err = Error::rate_limited("too many requests")
    .with_trace_id("abc-123")
    .with_retry_after(Duration::from_secs(30));

That's it. See the hero example above for Axum integration and validation patterns.

Framework Integration

Axum

With the axum-support feature, Error implements IntoResponse:

use axum::{Json, routing::get, Router};
use error_envelope::Error;

async fn handler() -> Result<Json<User>, Error> {
    let user = db::find_user("123").await?;
    Ok(Json(user))
}

// Error automatically converts to HTTP response with:
// - Correct status code
// - JSON body with error envelope
// - X-Request-ID header (if trace_id set)
// - Retry-After header (if retry_after set)

API Reference

Common constructors for typical scenarios:

use error_envelope::Error;

// Most common
Error::internal("Database connection failed");      // 500
Error::not_found("User not found");                 // 404
Error::unauthorized("Missing token");               // 401
Error::forbidden("Insufficient permissions");       // 403

// Validation
Error::bad_request("Invalid JSON");                 // 400
use error_envelope::validation;
let err = validation(field_errors);                 // 400 with field details

// Infrastructure
Error::rate_limited("Too many requests");           // 429
Error::timeout("Query timeout");                    // 504

Builder pattern:

let err = Error::rate_limited("too many requests")
    .with_details(serde_json::json!({"limit": 100}))
    .with_trace_id("trace-123")
    .with_retry_after(Duration::from_secs(30));

πŸ“š Full API documentation: API.md - Complete constructor reference, formatted helpers, advanced patterns

Error Codes

18 standard codes as a type-safe enum. Most common:

Code HTTP Status Use Case
Internal 500 Unexpected server errors
NotFound 404 Resource doesn't exist
Unauthorized 401 Missing/invalid auth
ValidationFailed 400 Invalid input data
Timeout 504 Gateway timeout (retryable)

πŸ“š Complete reference: ERROR_CODES.md - All 18 codes with detailed descriptions, use cases, and retryable behavior

Design Principles

Minimal, framework-agnostic core (~500 lines); integrations behind feature flags. See ARCHITECTURE.md for design rationale.

Examples

Complete working examples in the examples/ directory:

Run any example:

cargo run --example domain_errors --features axum-support
cargo run --example validation --features axum-support
cargo run --example rate_limiting --features axum-support
cargo run --example tracing --features axum-support

Testing

cargo test --all-features

License

MIT

About

Structured, traceable, retry-aware HTTP error responses for Rust APIs. Features anyhow and Axum integration with a framework-agnostic core.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Languages