//! 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 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::NetworkConfig; use crate::core::AppState; use crate::routes; use super::metrics::HttpMetrics; use super::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 [`middleware::request_context_middleware`] (contexte de requête) /// et [`middleware::auth_middleware`] (authentification JWT) 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 = Arc::clone(&app_state.metrics.http); 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> { let app_state = self.app_state.clone(); // Construit le router avec state + middleware + layers Tower. // // Ordre d'application en axum (LIFO) : le dernier .layer() ajouté est le premier exécuté. // // Courant de la requête : // 1. CatchPanicLayer (le plus externe) // 2. CorsLayer // 3. TraceLayer // 4. metrics_middleware (commence le chrono) // 5. request_context_middleware (init ID) // 6. auth_middleware (identifie l'utilisateur) // 7. Routes (handlers) let app: Router = routes::router() .with_state(app_state.clone()) // Identification de l'utilisateur (optionnelle ici) .layer(axum_middleware::from_fn_with_state( app_state.clone(), middleware::auth_middleware, )) // Initialisation du contexte (ID de requête, etc.) .layer(axum_middleware::from_fn( middleware::request_context_middleware, )) // Métriques (mesure de la durée totale de la requête) .layer(axum_middleware::from_fn_with_state( app_state.metrics.http.clone(), middleware::metrics_middleware, )) // Spans tracing par requête (method, uri, status, latency) // .layer(TraceLayer::new_for_http()) // trop verbeux depuis 0.7 .layer( TraceLayer::new_for_http() .on_request(()) .on_response(()) .on_eos(()) .on_failure(()), ) // 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(()) } }