use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; use serde::{Deserialize, Serialize}; use serde_json::json; /// Gestion centralisée des erreurs du stream server /// /// Hiérarchie d'erreurs pour un debugging efficace et une gestion robuste use std::fmt; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum AppError { // Configuration et initialisation ConfigError { message: String, }, InitializationError { message: String, }, // Erreurs réseau et connexion NetworkError { message: String, }, ConnectionClosed, ConnectionTimeout, // Erreurs audio et streaming AudioError { message: String, }, StreamingError { message: String, }, EncodingError { message: String, }, DecodingError { message: String, }, // Erreurs de codec UnsupportedCodec { codec: String, }, InvalidSampleRate { rate: u32, supported: Vec, }, InvalidChannelCount { channels: u8, }, InvalidBitrate { codec: String, bitrate: u32, min: u32, max: u32, }, ParameterMismatch { expected: String, received: String, }, // Erreurs de fichier et stockage FileError { message: String, }, NotFound { resource: String, }, NotFoundWithStreamId { resource: String, stream_id: String, }, StorageError { message: String, }, // Erreurs de sérialisation SerializationError, InvalidData { message: String, }, // Erreurs de ressources ResourceExhausted { resource: String, }, LimitExceeded { limit: String, current: u32, }, ListenerLimitExceeded { limit: u32, stream_id: String, }, NotEnoughData, InsufficientData { message: String, }, BufferOverflow, BufferNotFound { buffer_id: String, stream_id: Option, }, BufferFull { buffer_id: String, stream_id: Option, }, // Erreurs d'autorisation Unauthorized, Forbidden, SignatureError { message: String, }, // Erreurs de thread et concurrence AlreadyRunning, AlreadyProcessing { resource: String, }, ThreadError { message: String, }, // Erreurs de synchronisation NoSyncPoint { message: String, }, TimeSync { message: String, }, // Erreurs génériques InternalError { message: String, }, Internal { message: String, }, // Alias pour compatibilité ExternalServiceError { service: String, message: String, }, } impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AppError::ConfigError { message } => write!(f, "Configuration error: {}", message), AppError::InitializationError { message } => { write!(f, "Initialization error: {}", message) } AppError::NetworkError { message } => write!(f, "Network error: {}", message), AppError::ConnectionClosed => write!(f, "Connection closed"), AppError::ConnectionTimeout => write!(f, "Connection timeout"), AppError::AudioError { message } => write!(f, "Audio error: {}", message), AppError::StreamingError { message } => write!(f, "Streaming error: {}", message), AppError::EncodingError { message } => write!(f, "Encoding error: {}", message), AppError::DecodingError { message } => write!(f, "Decoding error: {}", message), AppError::UnsupportedCodec { codec } => write!(f, "Unsupported codec: {}", codec), AppError::InvalidSampleRate { rate, supported } => write!( f, "Invalid sample rate: {} (supported: {:?})", rate, supported ), AppError::InvalidChannelCount { channels } => { write!(f, "Invalid channel count: {}", channels) } AppError::InvalidBitrate { codec, bitrate, min, max, } => write!( f, "Invalid bitrate for {}: {} (supported: {}-{})", codec, bitrate, min, max ), AppError::ParameterMismatch { expected, received } => write!( f, "Parameter mismatch: expected {}, received {}", expected, received ), AppError::FileError { message } => write!(f, "File error: {}", message), AppError::NotFound { resource } => write!(f, "Not found: {}", resource), AppError::NotFoundWithStreamId { resource, stream_id, } => write!(f, "Not found: {} (stream_id: {})", resource, stream_id), AppError::StorageError { message } => write!(f, "Storage error: {}", message), AppError::SerializationError => write!(f, "Serialization error"), AppError::InvalidData { message } => write!(f, "Invalid data: {}", message), AppError::ResourceExhausted { resource } => { write!(f, "Resource exhausted: {}", resource) } AppError::LimitExceeded { limit, current } => { write!(f, "Limit exceeded: {} (current: {})", limit, current) } AppError::ListenerLimitExceeded { limit, stream_id } => write!( f, "Listener limit exceeded: {} (stream_id: {})", limit, stream_id ), AppError::NotEnoughData => write!(f, "Not enough data"), AppError::InsufficientData { message } => write!(f, "Insufficient data: {}", message), AppError::BufferOverflow => write!(f, "Buffer overflow"), AppError::BufferNotFound { buffer_id, stream_id, } => { if let Some(sid) = stream_id { write!(f, "Buffer not found: {} (stream_id: {})", buffer_id, sid) } else { write!(f, "Buffer not found: {}", buffer_id) } } AppError::BufferFull { buffer_id, stream_id, } => { if let Some(sid) = stream_id { write!(f, "Buffer full: {} (stream_id: {})", buffer_id, sid) } else { write!(f, "Buffer full: {}", buffer_id) } } AppError::Unauthorized => write!(f, "Unauthorized access"), AppError::Forbidden => write!(f, "Forbidden access"), AppError::SignatureError { message } => write!(f, "Signature error: {}", message), AppError::AlreadyRunning => write!(f, "Process already running"), AppError::AlreadyProcessing { resource } => { write!(f, "Already processing: {}", resource) } AppError::ThreadError { message } => write!(f, "Thread error: {}", message), AppError::NoSyncPoint { message } => write!(f, "No sync point: {}", message), AppError::TimeSync { message } => write!(f, "Time sync error: {}", message), AppError::InternalError { message } => write!(f, "Internal error: {}", message), AppError::Internal { message } => write!(f, "Internal error: {}", message), AppError::ExternalServiceError { service, message } => { write!(f, "External service error: {} - {}", service, message) } } } } impl std::error::Error for AppError {} impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, error_message) = match &self { AppError::ConfigError { message } => { (StatusCode::INTERNAL_SERVER_ERROR, message.clone()) } AppError::InitializationError { message } => { (StatusCode::INTERNAL_SERVER_ERROR, message.clone()) } AppError::NetworkError { message } => { (StatusCode::INTERNAL_SERVER_ERROR, message.clone()) } AppError::ConnectionClosed => ( StatusCode::INTERNAL_SERVER_ERROR, "Connection closed".to_string(), ), AppError::ConnectionTimeout => ( StatusCode::INTERNAL_SERVER_ERROR, "Connection timeout".to_string(), ), AppError::AudioError { message } => { (StatusCode::INTERNAL_SERVER_ERROR, message.clone()) } AppError::StreamingError { message } => { (StatusCode::INTERNAL_SERVER_ERROR, message.clone()) } AppError::EncodingError { message } => { (StatusCode::INTERNAL_SERVER_ERROR, message.clone()) } AppError::DecodingError { message } => { (StatusCode::INTERNAL_SERVER_ERROR, message.clone()) } AppError::UnsupportedCodec { codec } => ( StatusCode::BAD_REQUEST, format!("Unsupported codec: {}", codec), ), AppError::InvalidSampleRate { rate, supported } => ( StatusCode::BAD_REQUEST, format!("Invalid sample rate: {} (supported: {:?})", rate, supported), ), AppError::InvalidChannelCount { channels } => ( StatusCode::BAD_REQUEST, format!("Invalid channel count: {}", channels), ), AppError::InvalidBitrate { codec, bitrate, min, max, } => ( StatusCode::BAD_REQUEST, format!( "Invalid bitrate for {}: {} (supported: {}-{})", codec, bitrate, min, max ), ), AppError::ParameterMismatch { expected, received } => ( StatusCode::BAD_REQUEST, format!( "Parameter mismatch: expected {}, received {}", expected, received ), ), AppError::FileError { message } => (StatusCode::INTERNAL_SERVER_ERROR, message.clone()), AppError::NotFound { resource } => { (StatusCode::NOT_FOUND, format!("Not found: {}", resource)) } AppError::StorageError { message } => { (StatusCode::INTERNAL_SERVER_ERROR, message.clone()) } AppError::SerializationError => ( StatusCode::INTERNAL_SERVER_ERROR, "Serialization error".to_string(), ), AppError::InvalidData { message } => (StatusCode::BAD_REQUEST, message.clone()), AppError::ResourceExhausted { resource } => ( StatusCode::INTERNAL_SERVER_ERROR, format!("Resource exhausted: {}", resource), ), AppError::LimitExceeded { limit, current } => ( StatusCode::TOO_MANY_REQUESTS, format!("Limit exceeded: {} (current: {})", limit, current), ), AppError::ListenerLimitExceeded { limit, stream_id } => ( StatusCode::TOO_MANY_REQUESTS, format!( "Listener limit exceeded: {} (stream_id: {})", limit, stream_id ), ), AppError::NotEnoughData => (StatusCode::BAD_REQUEST, "Not enough data".to_string()), AppError::InsufficientData { message } => (StatusCode::BAD_REQUEST, message.clone()), AppError::BufferOverflow => (StatusCode::BAD_REQUEST, "Buffer overflow".to_string()), AppError::BufferNotFound { buffer_id, stream_id, } => { let msg = if let Some(sid) = stream_id { format!("Buffer not found: {} (stream_id: {})", buffer_id, sid) } else { format!("Buffer not found: {}", buffer_id) }; (StatusCode::NOT_FOUND, msg) } AppError::BufferFull { buffer_id, stream_id, } => { let msg = if let Some(sid) = stream_id { format!("Buffer full: {} (stream_id: {})", buffer_id, sid) } else { format!("Buffer full: {}", buffer_id) }; (StatusCode::BAD_REQUEST, msg) } AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized access".to_string()), AppError::Forbidden => (StatusCode::FORBIDDEN, "Forbidden access".to_string()), AppError::SignatureError { message } => (StatusCode::UNAUTHORIZED, message.clone()), AppError::AlreadyRunning => { (StatusCode::CONFLICT, "Process already running".to_string()) } AppError::AlreadyProcessing { resource } => ( StatusCode::CONFLICT, format!("Already processing: {}", resource), ), AppError::ThreadError { message } => { (StatusCode::INTERNAL_SERVER_ERROR, message.clone()) } AppError::NoSyncPoint { message } => { (StatusCode::INTERNAL_SERVER_ERROR, message.clone()) } AppError::TimeSync { message } => (StatusCode::INTERNAL_SERVER_ERROR, message.clone()), AppError::InternalError { message } => { (StatusCode::INTERNAL_SERVER_ERROR, message.clone()) } AppError::Internal { message } => (StatusCode::INTERNAL_SERVER_ERROR, message.clone()), AppError::ExternalServiceError { service, message } => ( StatusCode::INTERNAL_SERVER_ERROR, format!("External service error: {} - {}", service, message), ), AppError::NotFoundWithStreamId { resource, stream_id, } => ( StatusCode::NOT_FOUND, format!("Not found: {} (stream_id: {})", resource, stream_id), ), }; let body = Json(json!({ "error": error_message.clone(), "status": status.as_u16(), })); (status, body).into_response() } } pub type Result = std::result::Result; /// Types d'erreurs spécifiques au streaming #[derive(Debug, Clone, Serialize, Deserialize)] pub enum StreamError { FileNotFound, InvalidRange, StreamError(String), } impl fmt::Display for StreamError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { StreamError::FileNotFound => write!(f, "File not found"), StreamError::InvalidRange => write!(f, "Invalid range"), StreamError::StreamError(msg) => write!(f, "Stream error: {}", msg), } } } impl std::error::Error for StreamError {} /// Conversion de StreamError vers AppError impl From for AppError { fn from(err: StreamError) -> Self { match err { StreamError::FileNotFound => AppError::NotFound { resource: "stream_file".to_string(), }, StreamError::InvalidRange => AppError::InvalidData { message: "Invalid range request".to_string(), }, StreamError::StreamError(msg) => AppError::StreamingError { message: msg }, } } } /// Conversion de AppError vers StreamError (quand applicable) impl From for StreamError { fn from(err: AppError) -> Self { match err { AppError::NotFound { resource } => StreamError::FileNotFound, AppError::InvalidData { message } if message.contains("range") || message.contains("Range") => { StreamError::InvalidRange } AppError::StreamingError { message } => StreamError::StreamError(message), _ => StreamError::StreamError(format!("{}", err)), } } } /// Conversion depuis std::io::Error impl From for StreamError { fn from(err: std::io::Error) -> Self { match err.kind() { std::io::ErrorKind::NotFound => StreamError::FileNotFound, std::io::ErrorKind::InvalidInput => StreamError::InvalidRange, _ => StreamError::StreamError(format!("IO error: {}", err)), } } } /// Conversion depuis std::io::Error vers AppError impl From for AppError { fn from(err: std::io::Error) -> Self { match err.kind() { std::io::ErrorKind::NotFound => AppError::NotFound { resource: "file".to_string(), }, std::io::ErrorKind::InvalidInput => AppError::InvalidData { message: format!("Invalid input: {}", err), }, std::io::ErrorKind::PermissionDenied => AppError::Forbidden, _ => AppError::FileError { message: format!("IO error: {}", err), }, } } } /// Conversion depuis sqlx::Error impl From for AppError { fn from(err: sqlx::Error) -> Self { match err { sqlx::Error::Database(db_err) => AppError::ExternalServiceError { service: "database".to_string(), message: db_err.message().to_string(), }, sqlx::Error::RowNotFound => AppError::NotFound { resource: "database_row".to_string(), }, sqlx::Error::PoolClosed => AppError::ConnectionClosed, sqlx::Error::PoolTimedOut => AppError::ConnectionTimeout, _ => AppError::ExternalServiceError { service: "database".to_string(), message: format!("Database error: {}", err), }, } } } /// Conversion depuis serde_json::Error impl From for AppError { fn from(err: serde_json::Error) -> Self { AppError::SerializationError } } /// Conversion depuis tokio::time::error::Elapsed impl From for AppError { fn from(_: tokio::time::error::Elapsed) -> Self { AppError::ConnectionTimeout } } /// Implémentation de IntoResponse pour StreamError impl IntoResponse for StreamError { fn into_response(self) -> Response { let app_error: AppError = self.into(); app_error.into_response() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_stream_error_display() { let err1 = StreamError::FileNotFound; assert_eq!(format!("{}", err1), "File not found"); let err2 = StreamError::InvalidRange; assert_eq!(format!("{}", err2), "Invalid range"); let err3 = StreamError::StreamError("Test error".to_string()); assert_eq!(format!("{}", err3), "Stream error: Test error"); } #[test] fn test_stream_error_to_app_error() { let stream_err = StreamError::FileNotFound; let app_err: AppError = stream_err.into(); match app_err { AppError::NotFound { resource } => { assert_eq!(resource, "stream_file"); } _ => panic!("Expected NotFound variant"), } let stream_err2 = StreamError::InvalidRange; let app_err2: AppError = stream_err2.into(); match app_err2 { AppError::InvalidData { message } => { assert!(message.contains("range")); } _ => panic!("Expected InvalidData variant"), } let stream_err3 = StreamError::StreamError("Test message".to_string()); let app_err3: AppError = stream_err3.into(); match app_err3 { AppError::StreamingError { message } => { assert_eq!(message, "Test message"); } _ => panic!("Expected StreamingError variant"), } } #[test] fn test_app_error_to_stream_error() { let app_err = AppError::NotFound { resource: "test".to_string(), }; let stream_err: StreamError = app_err.into(); match stream_err { StreamError::FileNotFound => {} _ => panic!("Expected FileNotFound variant"), } let app_err2 = AppError::InvalidData { message: "Invalid range request".to_string(), }; let stream_err2: StreamError = app_err2.into(); match stream_err2 { StreamError::InvalidRange => {} _ => panic!("Expected InvalidRange variant"), } let app_err3 = AppError::StreamingError { message: "Stream failed".to_string(), }; let stream_err3: StreamError = app_err3.into(); match stream_err3 { StreamError::StreamError(msg) => { assert_eq!(msg, "Stream failed"); } _ => panic!("Expected StreamError variant"), } } #[test] fn test_io_error_conversions() { let io_err = std::io::Error::from(std::io::ErrorKind::NotFound); let stream_err: StreamError = io_err.into(); match stream_err { StreamError::FileNotFound => {} _ => panic!("Expected FileNotFound variant"), } let io_err2 = std::io::Error::from(std::io::ErrorKind::InvalidInput); let stream_err2: StreamError = io_err2.into(); match stream_err2 { StreamError::InvalidRange => {} _ => panic!("Expected InvalidRange variant"), } let io_err3 = std::io::Error::from(std::io::ErrorKind::NotFound); let app_err: AppError = io_err3.into(); match app_err { AppError::NotFound { resource } => { assert_eq!(resource, "file"); } _ => panic!("Expected NotFound variant"), } } #[tokio::test] async fn test_stream_error_into_response() { let stream_err = StreamError::FileNotFound; let response = stream_err.into_response(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } #[test] fn test_serde_json_error_conversion() { let json_err = serde_json::from_str::("invalid json").unwrap_err(); let app_err: AppError = json_err.into(); match app_err { AppError::SerializationError => {} _ => panic!("Expected SerializationError variant"), } } }