//! Serveur HTTP axum. //! //! [`HttpServer`] encapsule la configuration réseau, l'état applicatif, //! les métriques et les layers Tower. Il est construit dans [`crate::core::App`] //! et démarré via [`HttpServer::run`]. use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; use axum::middleware as axum_middleware; use axum::Router; use tokio::net::TcpListener; use tokio::sync::broadcast; use tower_http::catch_panic::CatchPanicLayer; use tower_http::cors::CorsLayer; use tower_http::trace::TraceLayer; use crate::config::{AppConfig, NetworkConfig}; use crate::core::AppState; use crate::http::OxRouter; use crate::routes; use super::metrics::{self, HttpMetrics}; use super::middleware::context_middleware; // ── Erreurs ─────────────────────────────────────────────────────────────────── /// Erreurs pouvant survenir pendant l'opération du serveur HTTP. #[derive(Debug, thiserror::Error)] pub enum HttpServerError { #[error("failed to bind TCP listener to {addr}: {source}")] Bind { addr: SocketAddr, #[source] source: std::io::Error, }, #[error("I/O error: {0}")] Io(#[from] std::io::Error), } // ── Struct ──────────────────────────────────────────────────────────────────── /// Serveur HTTP asynchrone basé sur axum. /// /// - Injecte [`AppState`] via le middleware (les handlers l'obtiendront via /// `State` quand ils seront implémentés ; `.with_state()` sera /// ajouté en conséquence). /// - Applique [`context_middleware`] (authentification JWT + contexte de /// requête) sur l'ensemble du router. /// - Empile les layers Tower : [`CatchPanicLayer`], [`CorsLayer`] (permissif), /// [`TraceLayer`]. /// - Collecte des métriques via [`HttpMetrics`] et les reporte périodiquement. /// - Supporte un shutdown gracieux via un [`broadcast::Sender`]. /// /// # Exemple /// ```no_run /// use oxspeak_server_lib::config::AppConfig; /// use oxspeak_server_lib::core::{App, AppState}; /// use oxspeak_server_lib::http::server::HttpServer; /// /// #[tokio::main] /// async fn main() { /// let config = AppConfig::load().unwrap(); /// // AppState construit via App::build(config) /// } /// ``` pub struct HttpServer { bind_addr: SocketAddr, app_state: AppState, metrics: Arc, shutdown_rx: broadcast::Receiver<()>, } impl HttpServer { /// Construit un [`HttpServer`] depuis la configuration et l'état applicatif. /// /// Retourne le serveur et un [`broadcast::Sender`] pour déclencher le /// shutdown gracieux depuis l'extérieur. pub fn new( network_config: &NetworkConfig, app_state: AppState, ) -> (Self, broadcast::Sender<()>) { let bind_addr = SocketAddr::new(network_config.host.into(), network_config.tcp_port); let metrics = HttpMetrics::new(); let (shutdown_tx, shutdown_rx) = broadcast::channel(1); ( Self { bind_addr, app_state, metrics, shutdown_rx, }, shutdown_tx, ) } /// Retourne une référence aux métriques du serveur. pub fn metrics(&self) -> &Arc { &self.metrics } /// Bind le listener TCP et démarre la boucle de service. /// /// La future se résout lorsqu'un signal de shutdown est reçu ou qu'une /// erreur I/O fatale survient. pub async fn run(mut self) -> Result<(), HttpServerError> { // Lance le reporter de métriques toutes les 30 secondes metrics::spawn_reporter(self.metrics.clone(), Duration::from_secs(30)); let metrics = self.metrics.clone(); let app_state = self.app_state.clone(); // Construit le router avec state + middleware + layers Tower. // // Ordre d'application en axum : chaque `.layer()` enveloppe le service // courant — le dernier appel devient la couche la plus externe. // // CatchPanicLayer (outermost — intercepte tout panic en dessous) // └─ CorsLayer (répond aux preflight avant auth) // └─ TraceLayer (span tracing sur la durée totale) // └─ context_middleware (JWT + RequestContext + métriques) // └─ routes (handlers) let app: Router = routes::router() .with_state(app_state.clone()) // Innermost : auth JWT, injection contexte, métriques .layer(axum_middleware::from_fn_with_state( app_state.clone(), move |state, req, next| { let metrics = metrics.clone(); async move { metrics.inc_request(); let started = std::time::Instant::now(); let response = context_middleware(state, req, next).await; let latency = started.elapsed(); metrics.record_response(response.status().as_u16(), latency); response } }, )) // Spans tracing par requête (method, uri, status, latency) .layer(TraceLayer::new_for_http()) // CORS permissif (à affiner en production) .layer(CorsLayer::permissive()) // Outermost : intercepte les panics et retourne une 500 propre .layer(CatchPanicLayer::new()); let listener = TcpListener::bind(self.bind_addr) .await .map_err(|source| HttpServerError::Bind { addr: self.bind_addr, source, })?; tracing::info!(addr = %self.bind_addr, "HTTP server listening"); axum::serve(listener, app) .with_graceful_shutdown(async move { let _ = self.shutdown_rx.recv().await; tracing::info!("HTTP server shutting down"); }) .await?; Ok(()) } }