Files
oxspeak_server/src/http/server.rs
T
2026-05-15 19:35:06 +02:00

169 lines
6.2 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 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<AppState>` 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<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 = 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<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> {
// 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(())
}
}