veza/veza-stream-server/src/error.rs
2025-12-03 20:36:56 +01:00

664 lines
22 KiB
Rust

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<u32>,
},
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<String>,
},
BufferFull {
buffer_id: String,
stream_id: Option<String>,
},
// 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<T> = std::result::Result<T, AppError>;
/// 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<StreamError> 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<AppError> 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<std::io::Error> 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<std::io::Error> 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<sqlx::Error> 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<serde_json::Error> for AppError {
fn from(err: serde_json::Error) -> Self {
AppError::SerializationError
}
}
/// Conversion depuis tokio::time::error::Elapsed
impl From<tokio::time::error::Elapsed> 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::<serde_json::Value>("invalid json").unwrap_err();
let app_err: AppError = json_err.into();
match app_err {
AppError::SerializationError => {}
_ => panic!("Expected SerializationError variant"),
}
}
}