### Axum Application with Ergonomic Error Handling Source: https://github.com/kosolabs/axum-anyhow/blob/main/README.md This example demonstrates a basic Axum application setup that utilizes axum-anyhow for handling different types of errors. It shows how to convert parsing errors to 400, database connection errors to 500, and missing user data to 404. ```rust use anyhow::Result; use axum::{extract::Path, routing::get, Json, Router}; use axum_anyhow::{ApiResult, OptionExt, ResultExt}; use std::collections::HashMap; #[tokio::main] async fn main() { let app = Router::new().route("/users/{id}", get(get_user_handler)); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); } #[derive(serde::Serialize, Clone)] struct User { id: u32, name: String, } async fn get_user_handler(Path(id): Path) -> ApiResult> { // Convert parsing errors to 400 Bad Request let id = parse_id(&id).context_bad_request(("Invalid User ID", "User ID must be a u32"))?; // Convert unexpected errors to 500 Internal Server Error let db = Database::connect()?; // Convert Option::None to 404 Not Found let user = db.get_user(&id).context_not_found("User Not Found")?; Ok(Json(user)) } // Mock database struct Database { users: HashMap, } impl Database { fn connect() -> Result { Ok(Database { users: HashMap::from([(1, "Alice"), (2, "Bob"), (3, "Eve")]), }) } fn get_user(&self, id: &u32) -> Option { self.users.get(id).map(|name| User { id: *id, name: name.to_string(), }) } } fn parse_id(id: &str) -> Result { Ok(id.parse::()?) } ``` -------------------------------- ### Example Usage of ErrorInterceptorLayer Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/error-interceptor-layer.md Demonstrates how to create and apply the ErrorInterceptorLayer to an Axum Router, enriching errors with request method and URI. ```rust use axum::Router; use axum_anyhow::ErrorInterceptorLayer; use serde_json::json; let enricher_layer = ErrorInterceptorLayer::new(|builder, ctx| { builder.meta(json!({ "method": ctx.method().as_str(), "uri": ctx.uri().to_string(), })) }); let app: Router = Router::new() .layer(enricher_layer); ``` -------------------------------- ### Basic Setup with ErrorInterceptorLayer Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/error-interceptor-layer.md Demonstrates setting up the ErrorInterceptorLayer to enrich logs with request method, URI, and user agent. This middleware should be applied to your Axum router. ```rust use axum::{ Router, routing::get }; use axum_anyhow::{ErrorInterceptorLayer, ApiResult}; use serde_json::json; #[tokio::main] async fn main() { // Create an error interceptor layer that adds request context let middleware = ErrorInterceptorLayer::new(|builder, ctx| { builder.meta(json!({ "method": ctx.method().as_str(), "uri": ctx.uri().to_string(), "user_agent": ctx.headers() .get("user-agent") .and_then(|v| v.to_str().ok()) .unwrap_or("unknown"), })) }); // Build the router with the error interceptor middleware let app: Router = Router::new() .route("/", get(handler)) .layer(middleware); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); } async fn handler() -> ApiResult { Ok("Hello!".to_string()) } ``` -------------------------------- ### CRUD Endpoint: Creating an Item Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/api-result.md Example of a create operation for a CRUD endpoint. Includes validation and database insertion, with context-specific error handling. ```rust use axum::{extract::Path, routing::{get, post}, Json, Router}; use axum_anyhow::{ApiResult, OptionExt, ResultExt}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone)] struct Item { id: u32, name: String, } // Create async fn create_item(Json(item): Json) -> ApiResult> { validate_item(&item) .context_unprocessable_entity(("Validation Error", "Invalid item data"))?; insert_item_to_db(&item) .context_internal(("Database Error", "Failed to create item"))?; Ok(Json(item)) } // Helper functions fn get_item_from_db(id: u32) -> Option { None } fn validate_item(item: &Item) -> Result<(), String> { Ok(()) } fn insert_item_to_db(item: &Item) -> Result { Ok(item.clone()) } let app = Router::new() .route("/items/:id", get(get_item)) .route("/items", post(create_item)); ``` -------------------------------- ### Basic Error Hook Example Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/configuration.md A simple example of an error hook that prints error details to stderr. ```rust use axum_anyhow::on_error; on_error(|err| { eprintln!("Error: {} ({}): {}", err.status(), err.title(), err.detail().unwrap_or("")); }); ``` -------------------------------- ### Basic Handler Example Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/index.md Demonstrates a basic handler using axum-anyhow for error conversion, including custom error parsing, default 500 error handling, and converting None to 404. ```APIDOC ## Basic Handler ### Description This example shows how to define a handler function that returns `ApiResult>`. It utilizes `context_bad_request` for parsing errors, `context_not_found` for handling missing resources, and relies on `axum_anyhow` for default error conversions. ### Code ```rust use anyhow::Result; use axum::extract::Path; use axum::routing::get; use axum::Json; use axum::Router; use axum_anyhow::{ApiResult, OptionExt, ResultExt}; // Assume User struct and Database struct with connect() and get_user() methods are defined elsewhere. // Assume parse_id function is defined elsewhere. async fn get_user(Path(id): Path) -> ApiResult> { // Parse with custom error let id = parse_id(&id) .context_bad_request(("Invalid User ID", "User ID must be a u32"))?; // Unexpected errors default to 500 let db = Database::connect()?; // Convert None to 404 let user = db.get_user(&id) .context_not_found("User Not Found")?; Ok(Json(user)) } ``` ``` -------------------------------- ### CRUD Endpoint Pattern Example Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/api-result.md Illustrates a common pattern for implementing CRUD endpoints using `ApiResult` for responses and context-based error handling. ```APIDOC ### CRUD Endpoints ```rust use axum::{extract::Path, routing::{get, post}, Json, Router}; use axum_anyhow::{ApiResult, OptionExt, ResultExt}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone)] struct Item { id: u32, name: String, } // Read async fn get_item(Path(id): Path) -> ApiResult> { get_item_from_db(id) .context_not_found(("Not Found", "Item not found")) } // Create async fn create_item(Json(item): Json) -> ApiResult> { validate_item(&item) .context_unprocessable_entity(("Validation Error", "Invalid item data"))?; insert_item_to_db(&item) .context_internal(("Database Error", "Failed to create item"))?; Ok(Json(item)) } // Helper functions fn get_item_from_db(id: u32) -> Option { None } fn validate_item(item: &Item) -> Result<(), String> { Ok(()) } fn insert_item_to_db(item: &Item) -> Result { Ok(item.clone()) } let app = Router::new() .route("/items/:id", get(get_item)) .route("/items", post(create_item)); ``` This example shows how to handle `Option` results for not found cases and other errors during item retrieval and creation. ``` -------------------------------- ### Complete Axum Application Configuration with Axum-Anyhow Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/configuration.md This snippet shows a full Axum application setup using axum-anyhow. It demonstrates initializing tracing, configuring error exposure based on the build environment, setting up a custom error logging hook, and applying error enrichment middleware. ```rust use axum::{Router, routing::get}; use axum_anyhow::{set_expose_errors, on_error, ErrorInterceptorLayer, ApiResult}; use serde_json::json; use uuid::Uuid; use tracing; #[tokio::main] async fn main() { // Initialize tracing tracing_subscriber::fmt().init(); // Configure error exposure based on environment #[cfg(debug_assertions)] set_expose_errors(true); #[cfg(not(debug_assertions))] set_expose_errors(false); // Set up error logging hook on_error(|err| { tracing::error!( status = %err.status(), title = err.title(), detail = ?err.detail(), "API error occurred" ); }); // Create error enrichment middleware let enricher = ErrorInterceptorLayer::new(|builder, ctx| { builder.meta(json!({ "request_id": Uuid::new_v4().to_string(), "method": ctx.method().as_str(), "uri": ctx.uri().to_string(), "timestamp": chrono::Utc::now().to_rfc3339(), })) }); // Build router with error enrichment let app: Router = Router::new() .route("/api/users", get(list_users)) .route("/api/users/:id", get(get_user)) .layer(enricher); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000") .await .unwrap(); axum::serve(listener, app).await.unwrap(); } async fn list_users() -> ApiResult { Ok("Users".to_string()) } async fn get_user() -> ApiResult { Ok("User".to_string()) } ``` -------------------------------- ### Basic Error Hook Example Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/hook-system.md Demonstrates setting up a basic error hook that prints the error status, title, and detail to stderr. This hook is triggered when `ApiErrorBuilder::build()` is invoked. ```rust use axum_anyhow::on_error; use axum::http::StatusCode; on_error(|err| { eprintln!("API Error: {} ({}): {}", err.status(), err.title(), err.detail().unwrap_or("")); }); // Any subsequent error creation will trigger this hook let error = axum_anyhow::ApiError::builder() .status(StatusCode::BAD_REQUEST) .title("Test Error") .detail("This is a test") .build(); ``` -------------------------------- ### Access Request Headers Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/error-interceptor-layer.md Use the `headers()` method to access request headers. This example shows how to extract the 'user-agent' header. ```rust use axum_anyhow::ErrorInterceptorLayer; let layer = ErrorInterceptorLayer::new(|builder, req| { let user_agent = req.headers() .get("user-agent") .and_then(|v| v.to_str().ok()) .unwrap_or("unknown"); builder.meta(serde_json::json!({ "user_agent": user_agent })) }); ``` -------------------------------- ### Complex Validation Pattern Example Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/api-result.md Demonstrates handling complex validation logic for user registration, including format validation and uniqueness checks, using `ApiResult` and context-based error reporting. ```APIDOC ### Complex Validation ```rust use axum_anyhow::{ApiResult, ResultExt}; use anyhow::anyhow; async fn register_user( username: String, email: String, password: String, ) -> ApiResult { // Validate format validate_username(&username) .context_unprocessable_entity(( "Invalid Username", "Username must be 3-20 characters", ))?; validate_email(&email) .context_unprocessable_entity(( "Invalid Email", "Email format is invalid", ))?; validate_password(&password) .context_unprocessable_entity(( "Weak Password", "Password must be at least 8 characters", ))?; // Check uniqueness if user_exists(&email) .context_conflict(( "User Already Exists", "Email is already registered", ))? { return Err(anyhow!("User exists")) .context_conflict(("User Already Exists", "Email is already registered")) .map(|_| unreachable!()) .map_err(|e| e)?; } Ok("User registered".to_string()) } fn validate_username(name: &str) -> Result<(), String> { if name.len() >= 3 && name.len() <= 20 { Ok(()) } else { Err("Invalid".to_string()) } } fn validate_email(email: &str) -> Result<(), String> { if email.contains('@') { Ok(()) } else { Err("Invalid".to_string()) } } fn validate_password(pass: &str) -> Result<(), String> { if pass.len() >= 8 { Ok(()) } else { Err("Invalid".to_string()) } } fn user_exists(email: &str) -> Result { Ok(false) } ``` This example demonstrates how to chain validation results and report specific errors with appropriate HTTP status codes. ``` -------------------------------- ### With Custom Error Types Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/into-api-error.md Shows how any type implementing `Into` can be converted to `ApiError`, using a `std::io::Error` as an example with a NOT_FOUND status. ```APIDOC ## With Custom Error Types This example demonstrates that any type implementing `Into` can be converted into an `ApiError`. Here, a `std::io::Error` is converted to an `ApiError` with a NOT_FOUND status. ### Method This is an example of a method call on an existing error type, not a direct HTTP endpoint. ### Usage ```rust use axum_anyhow::IntoApiError; use std::io; fn read_file() -> io::Result { Err(io::Error::new(io::ErrorKind::NotFound, "file not found")) } let io_err = read_file().unwrap_err(); let api_err = io_err.context_not_found(("Not Found", "File does not exist")); assert_eq!(api_err.status(), axum::http::StatusCode::NOT_FOUND); ``` ### Response - **api_err** (`ApiError`) - The resulting ApiError object with NOT_FOUND status and associated context. ``` -------------------------------- ### ApiErrorContext Conversions and Usage Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/types.md Demonstrates how to use ApiErrorContext with ResultExt for error handling. Shows examples of setting only the title and setting both title and detail. ```rust use axum_anyhow::ResultExt; use anyhow::anyhow; fn fallible() -> anyhow::Result<()> { Err(anyhow!("oops")) } // Title only — detail is None let _: axum_anyhow::ApiResult<()> = fallible().context_not_found("Not Found"); // Title + detail let _: axum_anyhow::ApiResult<()> = fallible() .context_not_found(("Not Found", "The resource does not exist")); ``` -------------------------------- ### Handle User Request with Error Handling Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/helper-functions.md This example demonstrates using `bad_request` and `not_found` within an async handler to return specific API errors based on input parsing and data retrieval. ```rust use axum::{extract::Path, routing::get, Json, Router}; use axum_anyhow::{ApiResult, not_found, bad_request}; use std::collections::HashMap; #[derive(serde::Serialize)] struct User { id: u32, name: String, } async fn get_user(Path(id): Path) -> ApiResult> { let id: u32 = id.parse() .map_err(|_| bad_request("Invalid ID", "User ID must be a number"))?; let users: HashMap = HashMap::from([(1, "Alice")]); let user = users.get(&id) .ok_or_else(|| not_found("Not Found", "User does not exist"))?; Ok(Json(User { id, name: user.to_string(), })) } ``` -------------------------------- ### CRUD Endpoint: Reading an Item Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/api-result.md Example of a read operation for a CRUD endpoint. Uses `context_not_found` to handle cases where an item is not found. ```rust use axum::{extract::Path, routing::{get, post}, Json, Router}; use axum_anyhow::{ApiResult, OptionExt, ResultExt}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone)] struct Item { id: u32, name: String, } // Read async fn get_item(Path(id): Path) -> ApiResult> { get_item_from_db(id) .context_not_found(("Not Found", "Item not found")) } // Helper functions fn get_item_from_db(id: u32) -> Option { None } fn validate_item(item: &Item) -> Result<(), String> { Ok(()) } fn insert_item_to_db(item: &Item) -> Result { Ok(item.clone()) } let app = Router::new() .route("/items/:id", get(get_item)) .route("/items", post(create_item)); ``` -------------------------------- ### Error Conversion Chain Example Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/api-result.md Illustrates chaining different error conversions for file operations, converting IO errors to 500 Internal Server Errors and parse errors to 400 Bad Request. ```APIDOC ### Error Conversion Chain ```rust use axum_anyhow::{ApiResult, ResultExt}; async fn process_file(path: &str) -> ApiResult { // Convert IO errors to 500 let content = std::fs::read_to_string(path) .context_internal(( "File Error", "Failed to read file", ))?; // Convert parse errors to 400 let data: serde_json::Value = serde_json::from_str(&content) .context_bad_request(( "Invalid JSON", "File contains invalid JSON", ))?; Ok(data.to_string()) } ``` This example shows how to map different types of underlying errors (like file I/O or JSON parsing) to appropriate API error responses. ``` -------------------------------- ### Access Request URI Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/error-interceptor-layer.md Use the `uri()` method to get the URI of the request. This can be used to log the requested path and query parameters. ```rust use axum_anyhow::ErrorInterceptorLayer; let layer = ErrorInterceptorLayer::new(|builder, req| { let uri = req.uri(); builder.meta(serde_json::json!({ "uri": uri.to_string() })) }); ``` -------------------------------- ### Example Error Response Format Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/error-interceptor-layer.md This JSON shows the structure of an error response after being enriched by the ErrorInterceptorLayer. It includes standard error fields and custom metadata from the request. ```json { "status": 404, "title": "User Not Found", "detail": "No user with that ID", "meta": { "method": "GET", "uri": "/users/123", "user_agent": "Mozilla/5.0...", "request_id": "abc-123" } } ``` -------------------------------- ### Access Request Method Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/error-interceptor-layer.md Use the `method()` method to get the HTTP method of the request. This is useful for logging or conditional logic. ```rust use axum_anyhow::ErrorInterceptorLayer; let layer = ErrorInterceptorLayer::new(|builder, req| { let method = req.method(); builder.meta(serde_json::json!({ "method": method.as_str() })) }); ``` -------------------------------- ### External Monitoring Service Integration (Sentry) Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/hook-system.md Sends API errors to an external monitoring service like Sentry. This example shows how to capture a message with error details. ```rust use axum_anyhow::on_error; on_error(|err| { // Sentry sentry::capture_message( &format!("{}: {}", err.title(), err.detail().unwrap_or("")), sentry::Level::Error, ); }); ``` -------------------------------- ### Error Hook Usage Examples Source: https://github.com/kosolabs/axum-anyhow/blob/main/README.md Demonstrates various ways `ApiError` can be created, all of which will trigger the registered error hook. The hook receives the `ApiError` instance for processing. ```rust use axum_anyhow::{on_error, bad_request, ApiError, IntoApiError, ResultExt}; use anyhow::anyhow; use axum::http::StatusCode; // Set up the hook once at application startup on_error(|err| { eprintln!("Error occurred: {}", err.detail().unwrap_or("")); }); // The hook will be called for all of these: let error1: ApiError = bad_request("Invalid Input", "Name is required"); let result: ApiError = anyhow!("Database error") .context_internal(("Internal Error", "Failed to connect")); let error2 = ApiError::builder() .status(StatusCode::NOT_FOUND) .title("Not Found") .detail("Resource missing") .build(); ``` -------------------------------- ### External Monitoring Service Integration (Datadog) Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/hook-system.md Sends API errors to an external monitoring service like Datadog. This example demonstrates logging an error with associated status information. ```rust use axum_anyhow::on_error; on_error(|err| { // Datadog datadog::log( "error", &format!("{}: {}", err.title(), err.detail().unwrap_or("")), Some(vec![ ("status".to_string(), err.status().as_str().to_string()), ]), ); }); ``` -------------------------------- ### Basic Handler with Axum Anyhow Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/index.md Demonstrates a basic handler function using Axum Anyhow to parse IDs, connect to a database, and retrieve user data. It shows how to convert errors to specific HTTP status codes like 400 (Bad Request) and 404 (Not Found). ```rust use anyhow::Result; use axum::{extract::Path, routing::get, Json, Router}; use axum_anyhow::{ApiResult, OptionExt, ResultExt}; async fn get_user(Path(id): Path) -> ApiResult> { // Parse with custom error let id = parse_id(&id) .context_bad_request(("Invalid User ID", "User ID must be a u32"))?; // Unexpected errors default to 500 let db = Database::connect()?; // Convert None to 404 let user = db.get_user(&id) .context_not_found("User Not Found")?; Ok(Json(user)) } ``` -------------------------------- ### Importing Public Types from Crate Root Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/types.md Demonstrates how to import all public types from the crate root, simplifying usage. ```rust use axum_anyhow::{ApiError, ApiResult, ResultExt, OptionExt}; ``` -------------------------------- ### Exported Functions Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/README.md Provides helper functions for common HTTP status codes, error hooking, configuration, and middleware setup. ```APIDOC ## Exported Functions ### Status Code Helpers - `bad_request()` - `unauthorized()` - `forbidden()` - `not_found()` - `method_not_allowed()` - `conflict()` - `internal_server_error()` - `not_implemented()` - `service_unavailable()` - `gateway_timeout()` - `unprocessable_entity()` - `too_many_requests()` ### Error Hooks & Configuration - `on_error()`: For setting up error hooks. - `set_expose_errors()`: Enables exposing error details. - `is_expose_errors_enabled()`: Checks if error details exposure is enabled. ### Middleware - `ErrorInterceptorLayer::new()`: Constructs a new error interceptor layer. ``` -------------------------------- ### Complete Error Tracking with Axum-Anyhow Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/hook-system.md This snippet demonstrates how to set up a comprehensive error tracking hook using `axum_anyhow::on_error`. It includes incrementing an atomic counter for errors and logging them with `tracing` at different levels based on HTTP status codes. This is useful for monitoring application health and identifying error patterns. ```rust use axum::{Router, routing::get, Json}; use axum_anyhow::{on_error, ApiResult, bad_request}; use serde_json::json; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use tracing; // Track error count let error_count = Arc::new(AtomicU64::new(0)); // Set up comprehensive error hook let error_count_clone = error_count.clone(); on_error(move |err| { // Increment counter let count = error_count_clone.fetch_add(1, Ordering::SeqCst) + 1; // Log with appropriate level match err.status().as_u16() { 400..=499 => { tracing::warn!( status = %err.status(), title = err.title(), detail = ?err.detail(), count = count, "Client error" ); } 500..=599 => { tracing::error!( status = %err.status(), title = err.title(), detail = ?err.detail(), count = count, "Server error" ); } _ => { tracing::info!( status = %err.status(), title = err.title(), count = count, "API error" ); } } }); #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let app: Router = Router::new() .route("/api/test", get(handler)); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000") .await .unwrap(); axum::serve(listener, app).await.unwrap(); } async fn handler() -> ApiResult> { Err(bad_request("Test Error", "This is a test")) } ``` -------------------------------- ### With Title and Detail Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/into-api-error.md Demonstrates converting an `anyhow::Error` to an `ApiError` with both a title and a detail message. ```APIDOC ## With Title and Detail This example demonstrates converting an `anyhow::Error` into an `ApiError` by providing both a title and a detail message. This offers more specific information about the error. ### Method This is an example of a method call on an existing error type, not a direct HTTP endpoint. ### Usage ```rust use anyhow::anyhow; use axum_anyhow::IntoApiError; let err = anyhow!("Original error"); let api_err = err.context_bad_request(("Bad Request", "Field validation failed")); assert_eq!(api_err.title(), "Bad Request"); assert_eq!(api_err.detail(), Some("Field validation failed")); ``` ### Response - **api_err** (`ApiError`) - The resulting ApiError object with both a title and a detail message. ``` -------------------------------- ### Combine Axum-Anyhow with Anyhow Context Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/errors.md Integrate Axum-Anyhow with `anyhow::Context` to create error chains that include HTTP status information. Use `.context_internal()` for this purpose. ```rust use anyhow::{Context, Result}; use axum_anyhow::ResultExt; fn read_config() -> Result { std::fs::read_to_string("config.json") .context("Failed to read configuration file") } async fn handler() -> axum_anyhow::ApiResult { read_config() .context_internal(("Config Error", "Unable to load configuration")) } ``` -------------------------------- ### Converting Custom Error Types Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/into-api-error.md Convert any type that implements Into into an ApiError. This example shows converting a standard library io::Error. ```rust use axum_anyhow::IntoApiError; use std::io; fn read_file() -> io::Result { Err(io::Error::new(io::ErrorKind::NotFound, "file not found")) } let io_err = read_file().unwrap_err(); let api_err = io_err.context_not_found(("Not Found", "File does not exist")); assert_eq!(api_err.status(), axum::http::StatusCode::NOT_FOUND); ``` -------------------------------- ### Using OptionExt with Question Mark Operator Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/option-ext.md Demonstrates how OptionExt integrates with the '?' operator for concise error handling in async handlers. Ensure OptionExt is imported. ```rust use axum_anyhow::{ApiResult, OptionExt}; async fn handler(id: u32) -> ApiResult { let user = find_user(id).context_not_found("User Not Found")?; Ok(user) } fn find_user(id: u32) -> Option { if id == 1 { Some("Alice".to_string()) } else { None } } ``` -------------------------------- ### Axum Router with Consistent ApiResult Handlers Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/api-result.md Illustrates setting up an Axum router where multiple handlers consistently use ApiResult for their return types. ```rust use axum::{Router, routing::get}; use axum_anyhow::ApiResult; use axum::Json; async fn list_items() -> ApiResult>> { Ok(Json(vec![])) } async fn get_item(id: u32) -> ApiResult> { Ok(Json("item".to_string())) } async fn create_item(body: Json) -> ApiResult> { Ok(Json(body.0)) } let app = Router::new() .route("/items", get(list_items)) .route("/items/:id", get(get_item)) .route("/items", axum::routing::post(create_item)); ``` -------------------------------- ### Basic Axum Handler with ApiResult Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/api-result.md Demonstrates a simple Axum handler returning ApiResult> for a successful response. ```rust use axum::Json; use axum_anyhow::ApiResult; use serde::Serialize; #[derive(Serialize)] struct Response { message: String, } async fn handler() -> ApiResult> { Ok(Json(Response { message: "Success".to_string(), })) } ``` -------------------------------- ### Handle Conflict Errors Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/errors.md Generate a 409 Conflict error when an operation cannot be completed due to a resource conflict, such as a duplicate entry. This example checks for email uniqueness before creating a user. ```rust use axum_anyhow::{ApiResult, ResultExt}; use anyhow::anyhow; fn create_user(email: &str) -> ApiResult<()> { check_email_unique(email) .map_err(|_| anyhow!("Email already exists")) .context_conflict(("Conflict", "A user with this email already exists")) .map(|_| ()); } fn check_email_unique(email: &str) -> Result<(), String> { Err("Email exists".to_string()) } ``` -------------------------------- ### Convert None to 503 Service Unavailable Error Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/option-ext.md Use `context_service_unavailable` when a service is down, converting `None` to an HTTP 503 Service Unavailable error. Requires `axum_anyhow::OptionExt` import. ```rust use axum_anyhow::OptionExt; fn get_service_status() -> Option { // Returns None if service is unavailable None } let result = get_service_status() .context_service_unavailable(("Service Unavailable", "Service is temporarily down for maintenance")); assert_eq!(result.unwrap_err().status(), axum::http::StatusCode::SERVICE_UNAVAILABLE); ``` -------------------------------- ### Enable Error Detail Exposure via Environment Variable Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/index.md Enable error detail exposure by setting the `AXUM_ANYHOW_EXPOSE_ERRORS` environment variable to `true`. ```bash AXUM_ANYHOW_EXPOSE_ERRORS=true cargo run ``` -------------------------------- ### RFC 9457 JSON Error Response Example Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/index.md Illustrates the standard JSON format for API errors as defined by RFC 9457, including status, title, and detail fields. ```json { "status": 404, "title": "Not Found", "detail": "The requested resource does not exist" } ``` -------------------------------- ### detail() Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/api-error-builder.md Sets a detailed explanation of the error. Accepts `&str`, `String`, or any type implementing `Into`. If not set, the detail will be `None`. ```APIDOC ### `detail()` Sets the error detail message. **Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | detail | `impl Into` | Yes | — | The error detail | **Return type:** `Self` **Description:** Sets a detailed explanation of the error. Accepts `&str`, `String`, or any type implementing `Into`. If not set, the detail will be `None`. **Example:** ```rust use axum_anyhow::ApiError; use axum::http::StatusCode; let error = ApiError::builder() .status(StatusCode::FORBIDDEN) .title("Access Denied") .detail("You do not have permission to access this resource") .build(); assert_eq!(error.detail(), Some("You do not have permission to access this resource")); ``` ``` -------------------------------- ### Error Interceptor Layer with Request Metadata Enrichment Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/types.md Example of configuring an ErrorInterceptorLayer to enrich error responses with request metadata. It demonstrates accessing the method, URI, and user-agent header from the request context. ```rust use axum_anyhow::ErrorInterceptorLayer; use serde_json::json; let layer = ErrorInterceptorLayer::new(|builder, ctx| { builder.meta(json!({ "method": ctx.method().as_str(), "uri": ctx.uri().to_string(), "user_agent": ctx.headers() .get("user-agent") .and_then(|v| v.to_str().ok()) .unwrap_or("unknown"), })) }); ``` -------------------------------- ### No Hook Scenario Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/hook-system.md Demonstrates normal error creation when no hook is set in Axum-Anyhow. No additional overhead is incurred. ```rust use axum_anyhow::ApiError; use axum::http::StatusCode; // No on_error() call - no hook set let error = ApiError::builder() .status(StatusCode::NOT_FOUND) .title("Not Found") .build(); // No hook overhead ``` -------------------------------- ### Handle Validation Errors Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/errors.md Generate a validation error response when input data fails validation checks. This example demonstrates returning a 422 Unprocessable Entity error with specific validation details. ```rust use axum_anyhow::{ApiResult, ResultExt}; use anyhow::anyhow; fn validate_email(email: &str) -> ApiResult { if !email.contains('@') { return Err(anyhow!("Invalid email")) .context_unprocessable_entity(("Validation Error", "Email must contain @"))?; } Ok(email.to_string()) } ``` -------------------------------- ### Converting Option to ApiResult with Title and Detail Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/option-ext.md Illustrates converting an Option to an ApiResult with both a title and a detailed error message. Requires OptionExt import. ```rust use axum_anyhow::OptionExt; let option: Option = None; let api_result = option.context_not_found(("Not Found", "No item with that ID exists")); ``` -------------------------------- ### Convert None to 504 Gateway Timeout Error Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/option-ext.md Use `context_gateway_timeout` when upstream service timeouts occur, converting `None` to an HTTP 504 Gateway Timeout error. Requires `axum_anyhow::OptionExt` import. ```rust use axum_anyhow::OptionExt; fn wait_for_upstream_response(timeout_ms: u64) -> Option { // Returns None if timeout exceeded None } let result = wait_for_upstream_response(5000) .context_gateway_timeout(("Gateway Timeout", "Upstream service did not respond within timeout")); assert_eq!(result.unwrap_err().status(), axum::http::StatusCode::GATEWAY_TIMEOUT); ``` -------------------------------- ### Example Enriched Error JSON Response Source: https://github.com/kosolabs/axum-anyhow/blob/main/README.md This JSON structure illustrates how an error response appears after being processed by the ErrorInterceptorLayer. It includes standard error fields along with a 'meta' object containing request-specific details. ```json { "status": 404, "title": "User Not Found", "detail": "No user with that ID", "meta": { "method": "GET", "uri": "/users/123", "user_agent": "Mozilla/5.0...", "timestamp": "2024-01-01T12:00:00Z" } } ``` -------------------------------- ### Convert None to 404 Not Found Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/option-ext.md Use this method when a requested resource does not exist. It converts `None` into an `ApiResult` with a 404 Not Found status. ```rust use axum_anyhow::{ApiResult, OptionExt}; fn find_resource(id: u32) -> Option { None } let result = find_resource(1) .context_not_found("Not Found"); assert_eq!(result.unwrap_err().status(), axum::http::StatusCode::NOT_FOUND); ``` -------------------------------- ### meta() Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/api-error.md Returns the optional metadata object. ```APIDOC ## meta() ### Description Returns the optional metadata object. ### Method `pub fn meta(&self) -> Option<&Value>` ### Return type `Option<&serde_json::Value>` ### Details Returns metadata added via the builder's `meta()` method, if any. ### Example ```rust use axum_anyhow::ApiError; use axum::http::StatusCode; use serde_json::json; let error = ApiError::builder() .status(StatusCode::NOT_FOUND) .title("Not Found") .meta(json!({"request_id": "abc-123"})) .build(); assert!(error.meta().is_some()); assert_eq!(error.meta().unwrap()["request_id"], "abc-123"); ``` ``` -------------------------------- ### Enable Error Details via Environment Variable Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/errors.md Alternatively, error details can be exposed by setting the `AXUM_ANYHOW_EXPOSE_ERRORS` environment variable to `1` or `true`. This method is useful for configuring the application without code changes. ```bash AXUM_ANYHOW_EXPOSE_ERRORS=1 cargo run # or AXUM_ANYHOW_EXPOSE_ERRORS=true cargo run ``` -------------------------------- ### With Title Only Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/into-api-error.md Illustrates converting an `anyhow::Error` to an `ApiError` using only a title string for the context. ```APIDOC ## With Title Only This example shows how to convert an `anyhow::Error` into an `ApiError` by providing only a title string for the context. The detail will be `None`. ### Method This is an example of a method call on an existing error type, not a direct HTTP endpoint. ### Usage ```rust use anyhow::anyhow; use axum_anyhow::IntoApiError; let err = anyhow!("Invalid input"); let api_err = err.context_bad_request("Bad Request"); assert_eq!(api_err.title(), "Bad Request"); assert_eq!(api_err.detail(), None); ``` ### Response - **api_err** (`ApiError`) - The resulting ApiError object with a title but no detail. ``` -------------------------------- ### Building an API Error with Fluent Interface Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/api-error-builder.md Demonstrates constructing an ApiError using method chaining for status, title, detail, meta, and the underlying error. Includes assertions to verify the built error's properties. ```rust use axum_anyhow::ApiError; use axum::http::StatusCode; use anyhow::anyhow; use serde_json::json; let error = ApiError::builder() .status(StatusCode::CONFLICT) .title("Conflict") .detail("Resource already exists") .meta(json!({"duplicate_field": "email"})) .error(anyhow!("Unique constraint violation")) .build(); assert_eq!(error.status(), StatusCode::CONFLICT); assert_eq!(error.title(), "Conflict"); assert_eq!(error.detail(), Some("Resource already exists")); assert!(error.meta().is_some()); assert!(error.error().is_some()); ``` -------------------------------- ### Convert None to 502 Bad Gateway Error Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/option-ext.md Use `context_bad_gateway` when upstream response parsing fails, converting `None` to an HTTP 502 Bad Gateway error. Requires `axum_anyhow::OptionExt` import. ```rust use axum_anyhow::OptionExt; fn parse_upstream_response(response: &str) -> Option { // Returns None if response is invalid None } let result = parse_upstream_response("invalid") .context_bad_gateway(("Bad Gateway", "Upstream service returned invalid response")); assert_eq!(result.unwrap_err().status(), axum::http::StatusCode::BAD_GATEWAY); ``` -------------------------------- ### Cloning an API Error Builder Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/api-error-builder.md Shows how to clone an ApiErrorBuilder. Note that the `anyhow::Error` field is dropped during cloning because `anyhow::Error` itself does not implement `Clone`. ```rust use axum_anyhow::ApiError; use axum::http::StatusCode; let builder = ApiError::builder() .status(StatusCode::NOT_FOUND) .title("Not Found"); let cloned = builder.clone(); // The cloned builder has the same status and title let error = cloned.detail("Resource not found").build(); ``` -------------------------------- ### Convert None to 500 Internal Server Error Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/option-ext.md Use `context_internal` when a critical value is missing, converting `None` to an HTTP 500 Internal Server Error. Requires `axum_anyhow::OptionExt` import. ```rust use axum_anyhow::OptionExt; fn get_config_value(key: &str) -> Option { // Returns None if critical config is missing None } let result = get_config_value("database_url") .context_internal(("Internal Error", "Critical configuration missing")); assert_eq!(result.unwrap_err().status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR); ``` -------------------------------- ### Standard vs. Custom Error Handling Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/api-result.md Demonstrates returning `ApiResult` for standard responses and `ApiError` directly for custom error handling scenarios. ```APIDOC ## Standard vs. Custom Error Handling Demonstrates returning `ApiResult` for standard responses and `ApiError` directly for custom error handling scenarios. ### Handler Examples #### Standard Handler ```rust use axum_anyhow::ApiResult; async fn standard_handler() -> ApiResult { Ok("Success".to_string()) } ``` #### Custom Handler ```rust use axum::http::StatusCode; use axum_anyhow::ApiError; async fn custom_handler() -> Result { let value = Some("data"); value.ok_or_else(|| { ApiError::builder() .status(StatusCode::NOT_FOUND) .title("Not Found") .build() }) } ``` Both handlers integrate with Axum's response system. ``` -------------------------------- ### Methods Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/README.md Details the available methods on `ApiError`, its builder, conversion traits, and request snapshot accessors. ```APIDOC ## Methods ### `ApiError` Methods - `status()` - `title()` - `detail()` - `meta()` - `error()` - `into_error()` ### `ApiErrorBuilder` Methods - `status()` - `title()` - `detail()` - `error()` - `meta()` - `build()` ### Trait Methods - **`ResultExt`**: 13 error conversion methods. - **`OptionExt`**: 13 error conversion methods. - **`IntoApiError`**: 13 error conversion methods. ### `RequestSnapshot` Accessors - `method()` - `uri()` - `headers()` ### `ErrorInterceptorLayer` Construction - `new()` ``` -------------------------------- ### Add Metadata to Axum-Anyhow Errors Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/errors.md Enhance API errors by including custom metadata using the `.meta()` method on the `ApiError::builder`. This allows for richer error reporting. ```rust use axum_anyhow::ApiError; use axum::http::StatusCode; use serde_json::json; let error = ApiError::builder() .status(StatusCode::NOT_FOUND) .title("User Not Found") .detail("No user with the given ID") .meta(json!({ "request_id": "abc-123", "timestamp": "2024-01-01T12:00:00Z", "user_id": 42 })) .build(); ``` -------------------------------- ### context_not_found() Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/result-ext.md Converts an error to a 404 Not Found error. Use when a requested resource does not exist. ```APIDOC ## context_not_found() ### Description Converts an error to a 404 Not Found error. Use when a requested resource does not exist. ### Method Signature ```rust fn context_not_found(self, context: impl Into) -> ApiResult ``` ### Parameters #### Path Parameters - **context** (`impl Into`) - Required - Title and optional detail ### Return Type `ApiResult` ### Example ```rust use anyhow::{anyhow, Result}; use axum_anyhow::{ApiResult, ResultExt}; fn find_user(id: u32) -> Result { Err(anyhow!("User {} not found", id)) } let result = find_user(123) .context_not_found("Not Found"); assert_eq!(result.unwrap_err().status(), axum::http::StatusCode::NOT_FOUND); ``` ``` -------------------------------- ### Create Service Unavailable Error Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/api-reference/helper-functions.md Use this function to create a 503 Service Unavailable error. It requires a title and detail message. ```rust pub fn service_unavailable(title: &str, detail: &str) -> ApiError ``` ```rust use axum_anyhow::service_unavailable; use axum::http::StatusCode; let error = service_unavailable("Service Unavailable", "Database is currently under maintenance"); assert_eq!(error.status(), StatusCode::SERVICE_UNAVAILABLE); ``` -------------------------------- ### Axum Handler Returning ApiResult Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/index.md Demonstrates a typical Axum handler function returning an `ApiResult>`. This pattern is recommended for integrating `axum-anyhow` into your application. ```rust async fn handler() -> ApiResult> { Ok(Json(user)) } ``` -------------------------------- ### Create Axum-Anyhow Errors Directly Source: https://github.com/kosolabs/axum-anyhow/blob/main/_autodocs/errors.md Use helper functions like `bad_request` or the `ApiError::builder` to construct API errors for non-Result/Option cases. Both methods produce equivalent results. ```rust use axum_anyhow::{ApiError, bad_request}; use axum::http::StatusCode; // Using helper function let error = bad_request("Invalid Input", "Name cannot be empty"); // Using builder let error = ApiError::builder() .status(StatusCode::BAD_REQUEST) .title("Invalid Input") .detail("Name cannot be empty") .build(); // Both produce equivalent results ```