172 lines
6.1 KiB
Rust
172 lines
6.1 KiB
Rust
//! 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<AppState>` 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<HttpMetrics>,
|
|
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<HttpMetrics> {
|
|
&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(())
|
|
}
|
|
}
|