1use thiserror::Error;
7use axum::http::StatusCode;
8use axum::response::{IntoResponse, Response};
9use axum::Json;
10use serde_json::json;
11
12pub type HttpResult<T> = Result<T, HttpError>;
14
15#[derive(Error, Debug)]
17pub enum HttpError {
18 #[error("Server startup failed: {message}")]
19 StartupFailed { message: String },
20
21 #[error("Server shutdown failed: {message}")]
22 ShutdownFailed { message: String },
23
24 #[error("Configuration error: {message}")]
25 ConfigError { message: String },
26
27 #[error("Service resolution failed: {service}")]
28 ServiceResolutionFailed { service: String },
29
30 #[error("Request timeout")]
31 RequestTimeout,
32
33 #[error("Request too large: {size} bytes exceeds limit of {limit} bytes")]
34 RequestTooLarge { size: usize, limit: usize },
35
36 #[error("Invalid request: {message}")]
37 BadRequest { message: String },
38
39 #[error("Internal server error: {message}")]
40 InternalError { message: String },
41
42 #[error("Health check failed: {reason}")]
43 HealthCheckFailed { reason: String },
44
45 #[error("Database error: {message}")]
46 DatabaseError { message: String },
47
48 #[error("Validation error: {message}")]
49 ValidationError { message: String },
50
51 #[error("Resource not found: {resource}")]
52 NotFound { resource: String },
53
54 #[error("Resource already exists: {message}")]
55 Conflict { message: String },
56
57 #[error("Unauthorized access")]
58 Unauthorized,
59
60 #[error("Access forbidden: {message}")]
61 Forbidden { message: String },
62}
63
64impl HttpError {
65 pub fn startup<T: Into<String>>(message: T) -> Self {
67 HttpError::StartupFailed {
68 message: message.into()
69 }
70 }
71
72 pub fn shutdown<T: Into<String>>(message: T) -> Self {
74 HttpError::ShutdownFailed {
75 message: message.into()
76 }
77 }
78
79 pub fn config<T: Into<String>>(message: T) -> Self {
81 HttpError::ConfigError {
82 message: message.into()
83 }
84 }
85
86 pub fn service_resolution<T: Into<String>>(service: T) -> Self {
88 HttpError::ServiceResolutionFailed {
89 service: service.into()
90 }
91 }
92
93 pub fn bad_request<T: Into<String>>(message: T) -> Self {
95 HttpError::BadRequest {
96 message: message.into()
97 }
98 }
99
100 pub fn internal<T: Into<String>>(message: T) -> Self {
102 HttpError::InternalError {
103 message: message.into()
104 }
105 }
106
107 pub fn health_check<T: Into<String>>(reason: T) -> Self {
109 HttpError::HealthCheckFailed {
110 reason: reason.into()
111 }
112 }
113
114 pub fn database_error<T: Into<String>>(message: T) -> Self {
116 HttpError::DatabaseError {
117 message: message.into()
118 }
119 }
120
121 pub fn validation_error<T: Into<String>>(message: T) -> Self {
123 HttpError::ValidationError {
124 message: message.into()
125 }
126 }
127
128 pub fn not_found<T: Into<String>>(resource: T) -> Self {
130 HttpError::NotFound {
131 resource: resource.into()
132 }
133 }
134
135 pub fn conflict<T: Into<String>>(message: T) -> Self {
137 HttpError::Conflict {
138 message: message.into()
139 }
140 }
141
142 pub fn unauthorized() -> Self {
144 HttpError::Unauthorized
145 }
146
147 pub fn forbidden<T: Into<String>>(message: T) -> Self {
149 HttpError::Forbidden {
150 message: message.into()
151 }
152 }
153
154 pub fn internal_server_error<T: Into<String>>(message: T) -> Self {
156 HttpError::InternalError {
157 message: message.into()
158 }
159 }
160
161 pub fn status_code(&self) -> StatusCode {
163 match self {
164 HttpError::StartupFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
165 HttpError::ShutdownFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
166 HttpError::ConfigError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
167 HttpError::ServiceResolutionFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
168 HttpError::RequestTimeout => StatusCode::REQUEST_TIMEOUT,
169 HttpError::RequestTooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
170 HttpError::BadRequest { .. } => StatusCode::BAD_REQUEST,
171 HttpError::InternalError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
172 HttpError::HealthCheckFailed { .. } => StatusCode::SERVICE_UNAVAILABLE,
173 HttpError::DatabaseError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
174 HttpError::ValidationError { .. } => StatusCode::BAD_REQUEST,
175 HttpError::NotFound { .. } => StatusCode::NOT_FOUND,
176 HttpError::Conflict { .. } => StatusCode::CONFLICT,
177 HttpError::Unauthorized => StatusCode::UNAUTHORIZED,
178 HttpError::Forbidden { .. } => StatusCode::FORBIDDEN,
179 }
180 }
181
182 pub fn error_code(&self) -> &'static str {
184 match self {
185 HttpError::StartupFailed { .. } => "SERVER_STARTUP_FAILED",
186 HttpError::ShutdownFailed { .. } => "SERVER_SHUTDOWN_FAILED",
187 HttpError::ConfigError { .. } => "CONFIGURATION_ERROR",
188 HttpError::ServiceResolutionFailed { .. } => "SERVICE_RESOLUTION_FAILED",
189 HttpError::RequestTimeout => "REQUEST_TIMEOUT",
190 HttpError::RequestTooLarge { .. } => "REQUEST_TOO_LARGE",
191 HttpError::BadRequest { .. } => "BAD_REQUEST",
192 HttpError::InternalError { .. } => "INTERNAL_ERROR",
193 HttpError::HealthCheckFailed { .. } => "HEALTH_CHECK_FAILED",
194 HttpError::DatabaseError { .. } => "DATABASE_ERROR",
195 HttpError::ValidationError { .. } => "VALIDATION_ERROR",
196 HttpError::NotFound { .. } => "RESOURCE_NOT_FOUND",
197 HttpError::Conflict { .. } => "RESOURCE_CONFLICT",
198 HttpError::Unauthorized => "UNAUTHORIZED_ACCESS",
199 HttpError::Forbidden { .. } => "ACCESS_FORBIDDEN",
200 }
201 }
202}
203
204impl IntoResponse for HttpError {
206 fn into_response(self) -> Response {
207 let status = self.status_code();
208 let body = json!({
209 "error": {
210 "code": self.error_code(),
211 "message": self.to_string(),
212 "hint": match &self {
213 HttpError::RequestTooLarge { .. } => Some("Reduce request payload size"),
214 HttpError::RequestTimeout => Some("Retry the request"),
215 HttpError::BadRequest { .. } => Some("Check request format and parameters"),
216 HttpError::HealthCheckFailed { .. } => Some("Server may be starting up or experiencing issues"),
217 _ => None,
218 }
219 }
220 });
221
222 (status, Json(body)).into_response()
223 }
224}
225
226impl From<elif_core::app_config::ConfigError> for HttpError {
228 fn from(err: elif_core::app_config::ConfigError) -> Self {
229 HttpError::ConfigError {
230 message: err.to_string()
231 }
232 }
233}
234
235impl From<std::io::Error> for HttpError {
237 fn from(err: std::io::Error) -> Self {
238 HttpError::InternalError {
239 message: format!("IO error: {}", err)
240 }
241 }
242}
243
244impl From<hyper::Error> for HttpError {
246 fn from(err: hyper::Error) -> Self {
247 HttpError::InternalError {
248 message: format!("Hyper error: {}", err)
249 }
250 }
251}
252
253impl From<elif_orm::ModelError> for HttpError {
255 fn from(err: elif_orm::ModelError) -> Self {
256 match err {
257 elif_orm::ModelError::Database(msg) => HttpError::DatabaseError { message: msg },
258 elif_orm::ModelError::Validation(msg) => HttpError::ValidationError { message: msg },
259 elif_orm::ModelError::NotFound(resource) => HttpError::NotFound { resource },
260 elif_orm::ModelError::Serialization(msg) => HttpError::InternalError {
261 message: format!("Serialization error: {}", msg)
262 },
263 _ => HttpError::InternalError {
264 message: format!("ORM error: {}", err)
265 },
266 }
267 }
268}
269
270impl From<serde_json::Error> for HttpError {
272 fn from(err: serde_json::Error) -> Self {
273 HttpError::InternalError {
274 message: format!("JSON serialization error: {}", err)
275 }
276 }
277}
278
279impl From<sqlx::Error> for HttpError {
281 fn from(err: sqlx::Error) -> Self {
282 match err {
283 sqlx::Error::RowNotFound => HttpError::NotFound {
284 resource: "Resource not found in database".to_string()
285 },
286 sqlx::Error::Database(db_err) => {
287 let err_msg = db_err.message();
289 if err_msg.contains("unique constraint") || err_msg.contains("duplicate key") {
290 HttpError::Conflict {
291 message: "Resource already exists".to_string()
292 }
293 } else {
294 HttpError::DatabaseError {
295 message: format!("Database error: {}", err_msg)
296 }
297 }
298 },
299 _ => HttpError::DatabaseError {
300 message: format!("Database operation failed: {}", err)
301 },
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn test_error_creation() {
312 let error = HttpError::startup("Failed to bind to port");
313 assert!(matches!(error, HttpError::StartupFailed { .. }));
314 assert_eq!(error.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
315 assert_eq!(error.error_code(), "SERVER_STARTUP_FAILED");
316 }
317
318 #[test]
319 fn test_error_status_codes() {
320 assert_eq!(HttpError::bad_request("test").status_code(), StatusCode::BAD_REQUEST);
321 assert_eq!(HttpError::RequestTimeout.status_code(), StatusCode::REQUEST_TIMEOUT);
322 assert_eq!(
323 HttpError::RequestTooLarge { size: 100, limit: 50 }.status_code(),
324 StatusCode::PAYLOAD_TOO_LARGE
325 );
326 assert_eq!(
327 HttpError::health_check("Database unavailable").status_code(),
328 StatusCode::SERVICE_UNAVAILABLE
329 );
330 }
331
332 #[test]
333 fn test_error_codes() {
334 assert_eq!(HttpError::bad_request("test").error_code(), "BAD_REQUEST");
335 assert_eq!(HttpError::RequestTimeout.error_code(), "REQUEST_TIMEOUT");
336 assert_eq!(HttpError::internal("test").error_code(), "INTERNAL_ERROR");
337 }
338
339 #[test]
340 fn test_config_error_conversion() {
341 let config_error = elif_core::app_config::ConfigError::MissingEnvVar {
342 var: "TEST_VAR".to_string(),
343 };
344 let http_error = HttpError::from(config_error);
345 assert!(matches!(http_error, HttpError::ConfigError { .. }));
346 }
347
348 #[test]
349 fn test_io_error_conversion() {
350 let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
351 let http_error = HttpError::from(io_error);
352 assert!(matches!(http_error, HttpError::InternalError { .. }));
353 }
354}