stano-axum 0.1.0

Axum HTTP extractors, unified ApiError type, error-logging middleware, declarative request-authorization, and route auto-registration for the Stano platform
Documentation

stano-axum

Axum HTTP layer: custom extractors with detailed error logging, a unified ApiError type that maps service errors to HTTP responses, and a standard error response format.

Install

[dependencies]
stano-axum = { path = "../stano-axum" }
stano-common = { path = "../stano-common" }

API

Error Types

  • ApiError — HTTP response error type (implements IntoResponse).

    • JsonExtraction { status, body_text, rejection_type } — JSON deserialization failed.
    • PathExtraction { status, body_text, rejection_type } — path parameter extraction failed.
    • QueryExtraction { status, body_text, rejection_type } — query parameter extraction failed.
    • Service(ServiceError) — service layer error (mapped to HTTP via ServiceError status code).
    • Internal(String) — unhandled internal error (returns 500; details hidden from clients).

    Behavior when converted to HTTP response:

    • ServiceError::NotFound → 404 (no error logged).
    • ServiceError::InvalidInput → 400 (logged).
    • ServiceError::Conflict → 409 (logged).
    • ServiceError::Unauthorized → 401 (no error logged).
    • ServiceError::Forbidden → 403 (no error logged).
    • ServiceError::Internal or ApiError::Internal → 500 (logged, details hidden from response).
    • Extraction errors (JsonExtraction, etc.) → client error status with code: "INVALID_JSON" / "INVALID_PATH" / "INVALID_QUERY" (logged).
  • ErrorResponse — standard JSON error shape: { status, code, message, details?, request_id? }. Derives utoipa::ToSchema so it can be referenced directly in #[utoipa::path(responses(...))] blocks (see Usage Example).

    • new(status: u16, code: impl Into<...>, message: impl Into<String>) -> Self
    • with_details(mut self, details: impl Into<String>) -> Self
    • with_request_id(mut self, request_id: impl Into<String>) -> Self

Extractors

  • AppJson<T> — JSON extractor (like axum::Json, but with detailed error logging).

    • impl FromRequest<S> for AppJson<T> where T: DeserializeOwned
    • impl IntoResponse for AppJson<T> where T: Serialize — also usable as a response type.
  • AppPath<T> — path parameter extractor (like axum::Path).

    • impl FromRequestParts<S> for AppPath<T> where T: DeserializeOwned + Send
  • AppQuery<T> — query parameter extractor (like axum::Query).

    • impl FromRequestParts<S> for AppQuery<T> where T: DeserializeOwned + Send

Middleware

  • error_logging_middleware — logs ApiError with request context (method, URI, request ID).
    • Reads x-request-id from the request and logs any ApiError stored in response extensions.

Usage Example

use stano_axum::{AppJson, AppPath, ApiError, ErrorResponse};
use stano_common::{ServiceError, id_type};
use stano_launcher::{get, post};
use axum::extract::State;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use utoipa::ToSchema;

id_type!(UserId, uuid_v7);

#[derive(Serialize, Deserialize, ToSchema)]
pub struct UserRequest {
    email: String,
}

#[derive(Serialize, ToSchema)]
pub struct UserResponse {
    id: String,
    email: String,
}

// Simulate a service.
async fn create_user(req: UserRequest) -> Result<UserResponse, ServiceError> {
    if req.email.is_empty() {
        return Err(ServiceError::InvalidInput("email required".into()));
    }
    Ok(UserResponse {
        id: UserId::new().to_string(),
        email: req.email,
    })
}

// HTTP handler using custom extractors, annotated for OpenAPI generation.
// stano-launcher's #[post(...)]/#[get(...)]/etc. (see stano-launcher's README) replace
// #[utoipa::path(...)], inferring request_body/the 200 response/params(...) from the
// handler's signature — write #[post(...)] instead of #[utoipa::path], and the spec stays
// in sync automatically, with no separate `axum::Router::route` call.
#[post(
    path = "/api/users",
    responses((status = 400, body = ErrorResponse)),
)]
async fn create_user_handler(
    AppJson(req): AppJson<UserRequest>,
) -> Result<AppJson<UserResponse>, ApiError> {
    let user = create_user(req).await?; // ServiceError -> ApiError via impl From
    Ok(AppJson(user))
}

// Another handler using path extraction, with a bearer-auth requirement declared explicitly.
// `params(("user_id" = UserId, Path))` is inferred from `AppPath<UserId>` + the single
// `{user_id}` placeholder.
#[get(
    path = "/api/users/{user_id}",
    responses((status = 404, body = ErrorResponse)),
    security(("bearerAuth" = [])),
)]
async fn get_user(
    AppPath(user_id): AppPath<UserId>,
) -> Result<AppJson<UserResponse>, ApiError> {
    // Fetch user...
    Err(ServiceError::NotFound.into()) // -> 404 response
}

Notes

  • Error details privacy — 4xx responses include details field; 5xx responses do not (security: don't leak internals).
  • Request ID integration — the error_logging_middleware reads x-request-id from incoming requests. For the ID to be set on the request itself, the middleware stack must include SetRequestIdLayer (typically applied in stano-launcher). The middleware still works if the ID is missing (just logs request_id: None).
  • No feature flags — all APIs available.