use thiserror::Error;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde_json::json;
use crate::response::{ElifResponse, IntoElifResponse};
pub type HttpResult<T> = Result<T, HttpError>;
#[derive(Error, Debug)]
pub enum HttpError {
#[error("Server startup failed: {message}")]
StartupFailed { message: String },
#[error("Server shutdown failed: {message}")]
ShutdownFailed { message: String },
#[error("Configuration error: {message}")]
ConfigError { message: String },
#[error("Service resolution failed: {service}")]
ServiceResolutionFailed { service: String },
#[error("Request timeout")]
RequestTimeout,
#[error("Request too large: {size} bytes exceeds limit of {limit} bytes")]
RequestTooLarge { size: usize, limit: usize },
#[error("Invalid request: {message}")]
BadRequest { message: String },
#[error("Internal server error: {message}")]
InternalError { message: String },
#[error("Health check failed: {reason}")]
HealthCheckFailed { reason: String },
#[error("Database error: {message}")]
DatabaseError { message: String },
#[error("Validation error: {message}")]
ValidationError { message: String },
#[error("Resource not found: {resource}")]
NotFound { resource: String },
#[error("Resource already exists: {message}")]
Conflict { message: String },
#[error("Unauthorized access")]
Unauthorized,
#[error("Access forbidden: {message}")]
Forbidden { message: String },
}
impl HttpError {
pub fn startup<T: Into<String>>(message: T) -> Self {
HttpError::StartupFailed {
message: message.into()
}
}
pub fn shutdown<T: Into<String>>(message: T) -> Self {
HttpError::ShutdownFailed {
message: message.into()
}
}
pub fn config<T: Into<String>>(message: T) -> Self {
HttpError::ConfigError {
message: message.into()
}
}
pub fn service_resolution<T: Into<String>>(service: T) -> Self {
HttpError::ServiceResolutionFailed {
service: service.into()
}
}
pub fn bad_request<T: Into<String>>(message: T) -> Self {
HttpError::BadRequest {
message: message.into()
}
}
pub fn internal<T: Into<String>>(message: T) -> Self {
HttpError::InternalError {
message: message.into()
}
}
pub fn health_check<T: Into<String>>(reason: T) -> Self {
HttpError::HealthCheckFailed {
reason: reason.into()
}
}
pub fn database_error<T: Into<String>>(message: T) -> Self {
HttpError::DatabaseError {
message: message.into()
}
}
pub fn validation_error<T: Into<String>>(message: T) -> Self {
HttpError::ValidationError {
message: message.into()
}
}
pub fn not_found<T: Into<String>>(resource: T) -> Self {
HttpError::NotFound {
resource: resource.into()
}
}
pub fn conflict<T: Into<String>>(message: T) -> Self {
HttpError::Conflict {
message: message.into()
}
}
pub fn unauthorized() -> Self {
HttpError::Unauthorized
}
pub fn forbidden<T: Into<String>>(message: T) -> Self {
HttpError::Forbidden {
message: message.into()
}
}
pub fn internal_server_error<T: Into<String>>(message: T) -> Self {
HttpError::InternalError {
message: message.into()
}
}
pub fn timeout<T: Into<String>>(_message: T) -> Self {
HttpError::RequestTimeout
}
pub fn payload_too_large<T: Into<String>>(_message: T) -> Self {
HttpError::RequestTooLarge {
size: 0, limit: 0
}
}
pub fn payload_too_large_with_sizes<T: Into<String>>(_message: T, size: usize, limit: usize) -> Self {
HttpError::RequestTooLarge { size, limit }
}
pub fn with_detail<T: Into<String>>(self, _detail: T) -> Self {
self
}
pub fn status_code(&self) -> StatusCode {
match self {
HttpError::StartupFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
HttpError::ShutdownFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
HttpError::ConfigError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
HttpError::ServiceResolutionFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
HttpError::RequestTimeout => StatusCode::REQUEST_TIMEOUT,
HttpError::RequestTooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
HttpError::BadRequest { .. } => StatusCode::BAD_REQUEST,
HttpError::InternalError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
HttpError::HealthCheckFailed { .. } => StatusCode::SERVICE_UNAVAILABLE,
HttpError::DatabaseError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
HttpError::ValidationError { .. } => StatusCode::UNPROCESSABLE_ENTITY,
HttpError::NotFound { .. } => StatusCode::NOT_FOUND,
HttpError::Conflict { .. } => StatusCode::CONFLICT,
HttpError::Unauthorized => StatusCode::UNAUTHORIZED,
HttpError::Forbidden { .. } => StatusCode::FORBIDDEN,
}
}
pub fn error_code(&self) -> &'static str {
match self {
HttpError::StartupFailed { .. } => "SERVER_STARTUP_FAILED",
HttpError::ShutdownFailed { .. } => "SERVER_SHUTDOWN_FAILED",
HttpError::ConfigError { .. } => "CONFIGURATION_ERROR",
HttpError::ServiceResolutionFailed { .. } => "SERVICE_RESOLUTION_FAILED",
HttpError::RequestTimeout => "REQUEST_TIMEOUT",
HttpError::RequestTooLarge { .. } => "REQUEST_TOO_LARGE",
HttpError::BadRequest { .. } => "BAD_REQUEST",
HttpError::InternalError { .. } => "INTERNAL_ERROR",
HttpError::HealthCheckFailed { .. } => "HEALTH_CHECK_FAILED",
HttpError::DatabaseError { .. } => "DATABASE_ERROR",
HttpError::ValidationError { .. } => "VALIDATION_ERROR",
HttpError::NotFound { .. } => "RESOURCE_NOT_FOUND",
HttpError::Conflict { .. } => "RESOURCE_CONFLICT",
HttpError::Unauthorized => "UNAUTHORIZED_ACCESS",
HttpError::Forbidden { .. } => "ACCESS_FORBIDDEN",
}
}
}
impl IntoElifResponse for HttpError {
fn into_elif_response(self) -> ElifResponse {
let body = json!({
"error": {
"code": self.error_code(),
"message": self.to_string(),
"hint": match &self {
HttpError::RequestTooLarge { .. } => Some("Reduce request payload size"),
HttpError::RequestTimeout => Some("Retry the request"),
HttpError::BadRequest { .. } => Some("Check request format and parameters"),
HttpError::HealthCheckFailed { .. } => Some("Server may be starting up or experiencing issues"),
_ => None,
}
}
});
ElifResponse::with_status(self.status_code())
.json_value(body)
}
}
impl IntoResponse for HttpError {
fn into_response(self) -> Response {
let status = self.status_code();
let body = json!({
"error": {
"code": self.error_code(),
"message": self.to_string(),
"hint": match &self {
HttpError::RequestTooLarge { .. } => Some("Reduce request payload size"),
HttpError::RequestTimeout => Some("Retry the request"),
HttpError::BadRequest { .. } => Some("Check request format and parameters"),
HttpError::HealthCheckFailed { .. } => Some("Server may be starting up or experiencing issues"),
_ => None,
}
}
});
(status, Json(body)).into_response()
}
}
impl From<elif_core::ConfigError> for HttpError {
fn from(err: elif_core::ConfigError) -> Self {
HttpError::ConfigError {
message: err.to_string()
}
}
}
impl From<std::io::Error> for HttpError {
fn from(err: std::io::Error) -> Self {
HttpError::InternalError {
message: format!("IO error: {}", err)
}
}
}
impl From<hyper::Error> for HttpError {
fn from(err: hyper::Error) -> Self {
HttpError::InternalError {
message: format!("Hyper error: {}", err)
}
}
}
impl From<serde_json::Error> for HttpError {
fn from(err: serde_json::Error) -> Self {
HttpError::InternalError {
message: format!("JSON serialization error: {}", err)
}
}
}
#[cfg(feature = "orm")]
impl From<orm::ModelError> for HttpError {
fn from(err: orm::ModelError) -> Self {
match err {
orm::ModelError::NotFound(table) => HttpError::NotFound {
resource: table
},
orm::ModelError::Validation(msg) => HttpError::ValidationError {
message: msg
},
orm::ModelError::Database(msg) => HttpError::DatabaseError {
message: msg
},
orm::ModelError::Connection(msg) => HttpError::DatabaseError {
message: format!("Connection error: {}", msg)
},
orm::ModelError::Transaction(msg) => HttpError::DatabaseError {
message: format!("Transaction error: {}", msg)
},
orm::ModelError::Query(msg) => HttpError::BadRequest {
message: format!("Query error: {}", msg)
},
orm::ModelError::Schema(msg) => HttpError::InternalError {
message: format!("Schema error: {}", msg)
},
orm::ModelError::Migration(msg) => HttpError::InternalError {
message: format!("Migration error: {}", msg)
},
orm::ModelError::MissingPrimaryKey => HttpError::BadRequest {
message: "Missing or invalid primary key".to_string()
},
orm::ModelError::Relationship(msg) => HttpError::BadRequest {
message: format!("Relationship error: {}", msg)
},
orm::ModelError::Serialization(msg) => HttpError::InternalError {
message: format!("Serialization error: {}", msg)
},
orm::ModelError::Event(msg) => HttpError::InternalError {
message: format!("Event error: {}", msg)
},
}
}
}
#[cfg(feature = "orm")]
impl From<orm::QueryError> for HttpError {
fn from(err: orm::QueryError) -> Self {
match err {
orm::QueryError::InvalidSql(msg) => HttpError::BadRequest {
message: format!("Invalid SQL query: {}", msg)
},
orm::QueryError::MissingFields(msg) => HttpError::BadRequest {
message: format!("Missing required fields: {}", msg)
},
orm::QueryError::InvalidParameter(msg) => HttpError::BadRequest {
message: format!("Invalid query parameter: {}", msg)
},
orm::QueryError::UnsupportedOperation(msg) => HttpError::BadRequest {
message: format!("Unsupported operation: {}", msg)
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let error = HttpError::startup("Failed to bind to port");
assert!(matches!(error, HttpError::StartupFailed { .. }));
assert_eq!(error.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(error.error_code(), "SERVER_STARTUP_FAILED");
}
#[test]
fn test_error_status_codes() {
assert_eq!(HttpError::bad_request("test").status_code(), StatusCode::BAD_REQUEST);
assert_eq!(HttpError::RequestTimeout.status_code(), StatusCode::REQUEST_TIMEOUT);
assert_eq!(
HttpError::RequestTooLarge { size: 100, limit: 50 }.status_code(),
StatusCode::PAYLOAD_TOO_LARGE
);
assert_eq!(
HttpError::health_check("Database unavailable").status_code(),
StatusCode::SERVICE_UNAVAILABLE
);
}
#[test]
fn test_error_codes() {
assert_eq!(HttpError::bad_request("test").error_code(), "BAD_REQUEST");
assert_eq!(HttpError::RequestTimeout.error_code(), "REQUEST_TIMEOUT");
assert_eq!(HttpError::internal("test").error_code(), "INTERNAL_ERROR");
}
#[test]
fn test_config_error_conversion() {
let config_error = elif_core::ConfigError::validation_failed("Test validation error");
let http_error = HttpError::from(config_error);
assert!(matches!(http_error, HttpError::ConfigError { .. }));
}
#[test]
fn test_io_error_conversion() {
let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
let http_error = HttpError::from(io_error);
assert!(matches!(http_error, HttpError::InternalError { .. }));
}
#[cfg(feature = "orm")]
#[test]
fn test_orm_error_conversions() {
let not_found_error = orm::ModelError::NotFound("users".to_string());
let http_error = HttpError::from(not_found_error);
assert!(matches!(http_error, HttpError::NotFound { .. }));
assert_eq!(http_error.status_code(), StatusCode::NOT_FOUND);
let validation_error = orm::ModelError::Validation("Invalid email".to_string());
let http_error = HttpError::from(validation_error);
assert!(matches!(http_error, HttpError::ValidationError { .. }));
assert_eq!(http_error.status_code(), StatusCode::UNPROCESSABLE_ENTITY);
let database_error = orm::ModelError::Database("Connection failed".to_string());
let http_error = HttpError::from(database_error);
assert!(matches!(http_error, HttpError::DatabaseError { .. }));
assert_eq!(http_error.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
let query_error = orm::QueryError::InvalidSql("Syntax error".to_string());
let http_error = HttpError::from(query_error);
assert!(matches!(http_error, HttpError::BadRequest { .. }));
assert_eq!(http_error.status_code(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_validation_error_status_code() {
let validation_error = HttpError::validation_error("Field is required");
assert_eq!(validation_error.status_code(), StatusCode::UNPROCESSABLE_ENTITY);
assert_eq!(validation_error.error_code(), "VALIDATION_ERROR");
}
#[test]
fn test_error_response_format_consistency() {
use axum::response::IntoResponse as AxumIntoResponse;
let error = HttpError::not_found("User");
let response = AxumIntoResponse::into_response(error);
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
}