nido-svc-common 0.1.0-alpha.1

Shared health, error, OpenAPI, SSE, and middleware primitives for nido-*-svc crates
Documentation
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Canonical tower middleware stack for nido-*-svc crates.
//!
//! Layer order applied via repeated `Router::layer` (axum applies layers in
//! reverse insertion order, so the first layer listed is outermost at runtime):
//! 1. `SetSensitiveRequestHeadersLayer` — scrub Authorization from traces
//! 2. `SetRequestIdLayer`               — assign x-request-id (UUID v4)
//! 3. `TraceLayer`                      — structured HTTP tracing
//! 4. `PropagateRequestIdLayer`         — echo x-request-id in responses
//! 5. `TimeoutLayer`                    — 30s hard cap (returns 408)
//! 6. `CorsLayer`                       — permissive dev default; restrict in prod
//!
//! Compression is intentionally omitted: apply per-router excluding SSE routes
//! (compressed streaming buffers the stream and defeats SSE semantics).

use axum::http::{header::AUTHORIZATION, HeaderName, Request};
use std::time::Duration;
use tower_http::{
    cors::CorsLayer,
    request_id::{MakeRequestId, PropagateRequestIdLayer, RequestId, SetRequestIdLayer},
    sensitive_headers::SetSensitiveRequestHeadersLayer,
    timeout::TimeoutLayer,
    trace::TraceLayer,
};
use uuid::Uuid;

/// Default request timeout for all non-SSE routes.
pub const DEFAULT_TIMEOUT_SECS: u64 = 30;

const X_REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id");

/// Request ID generator using UUID v4.
#[derive(Clone, Copy)]
pub struct UuidRequestId;

impl MakeRequestId for UuidRequestId {
    fn make_request_id<B>(&mut self, _req: &Request<B>) -> Option<RequestId> {
        let hv = Uuid::new_v4().to_string().parse().ok()?;
        Some(RequestId::new(hv))
    }
}

/// Apply the canonical nido-*-svc middleware stack to an axum `Router`.
///
/// Layers are applied so that the canonical order is preserved:
/// sensitive-headers → request-id → trace → propagate-id → timeout → cors.
///
/// # Example
///
/// ```rust,no_run
/// use axum::{routing::get, Router};
/// use nido_svc_common::middleware::apply_middleware_stack;
///
/// let app = apply_middleware_stack(
///     Router::new().route("/ping", get(|| async { "pong" })),
///     "my-svc",
/// );
/// ```
pub fn apply_middleware_stack(router: axum::Router, _service_name: &'static str) -> axum::Router {
    // axum::Router::layer applies in reverse order: the last `.layer()` call
    // is outermost.  We list outermost-first and reverse by insertion order.
    router
        .layer(CorsLayer::permissive())
        .layer(TimeoutLayer::with_status_code(
            axum::http::StatusCode::REQUEST_TIMEOUT,
            Duration::from_secs(DEFAULT_TIMEOUT_SECS),
        ))
        .layer(PropagateRequestIdLayer::new(X_REQUEST_ID.clone()))
        .layer(TraceLayer::new_for_http())
        .layer(SetRequestIdLayer::new(X_REQUEST_ID.clone(), UuidRequestId))
        .layer(SetSensitiveRequestHeadersLayer::new([AUTHORIZATION]))
}

/// Returns `true` if the middleware stack assembles without panic.
///
/// Intended for use in smoke-test assertions.
pub fn middleware_stack_assembles(_service_name: &'static str) -> bool {
    // If we can call apply_middleware_stack on a dummy router, it compiled.
    let _ = apply_middleware_stack(axum::Router::new(), _service_name);
    true
}