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 timeout<T: Into<String>>(_message: T) -> Self {
163 HttpError::RequestTimeout
164 }
165
166 pub fn payload_too_large<T: Into<String>>(_message: T) -> Self {
168 HttpError::RequestTooLarge {
169 size: 0, limit: 0
171 }
172 }
173
174 pub fn payload_too_large_with_sizes<T: Into<String>>(_message: T, size: usize, limit: usize) -> Self {
176 HttpError::RequestTooLarge { size, limit }
177 }
178
179 pub fn with_detail<T: Into<String>>(self, _detail: T) -> Self {
181 self
182 }
183
184 pub fn status_code(&self) -> StatusCode {
186 match self {
187 HttpError::StartupFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
188 HttpError::ShutdownFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
189 HttpError::ConfigError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
190 HttpError::ServiceResolutionFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
191 HttpError::RequestTimeout => StatusCode::REQUEST_TIMEOUT,
192 HttpError::RequestTooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
193 HttpError::BadRequest { .. } => StatusCode::BAD_REQUEST,
194 HttpError::InternalError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
195 HttpError::HealthCheckFailed { .. } => StatusCode::SERVICE_UNAVAILABLE,
196 HttpError::DatabaseError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
197 HttpError::ValidationError { .. } => StatusCode::BAD_REQUEST,
198 HttpError::NotFound { .. } => StatusCode::NOT_FOUND,
199 HttpError::Conflict { .. } => StatusCode::CONFLICT,
200 HttpError::Unauthorized => StatusCode::UNAUTHORIZED,
201 HttpError::Forbidden { .. } => StatusCode::FORBIDDEN,
202 }
203 }
204
205 pub fn error_code(&self) -> &'static str {
207 match self {
208 HttpError::StartupFailed { .. } => "SERVER_STARTUP_FAILED",
209 HttpError::ShutdownFailed { .. } => "SERVER_SHUTDOWN_FAILED",
210 HttpError::ConfigError { .. } => "CONFIGURATION_ERROR",
211 HttpError::ServiceResolutionFailed { .. } => "SERVICE_RESOLUTION_FAILED",
212 HttpError::RequestTimeout => "REQUEST_TIMEOUT",
213 HttpError::RequestTooLarge { .. } => "REQUEST_TOO_LARGE",
214 HttpError::BadRequest { .. } => "BAD_REQUEST",
215 HttpError::InternalError { .. } => "INTERNAL_ERROR",
216 HttpError::HealthCheckFailed { .. } => "HEALTH_CHECK_FAILED",
217 HttpError::DatabaseError { .. } => "DATABASE_ERROR",
218 HttpError::ValidationError { .. } => "VALIDATION_ERROR",
219 HttpError::NotFound { .. } => "RESOURCE_NOT_FOUND",
220 HttpError::Conflict { .. } => "RESOURCE_CONFLICT",
221 HttpError::Unauthorized => "UNAUTHORIZED_ACCESS",
222 HttpError::Forbidden { .. } => "ACCESS_FORBIDDEN",
223 }
224 }
225}
226
227impl IntoResponse for HttpError {
229 fn into_response(self) -> Response {
230 let status = self.status_code();
231 let body = json!({
232 "error": {
233 "code": self.error_code(),
234 "message": self.to_string(),
235 "hint": match &self {
236 HttpError::RequestTooLarge { .. } => Some("Reduce request payload size"),
237 HttpError::RequestTimeout => Some("Retry the request"),
238 HttpError::BadRequest { .. } => Some("Check request format and parameters"),
239 HttpError::HealthCheckFailed { .. } => Some("Server may be starting up or experiencing issues"),
240 _ => None,
241 }
242 }
243 });
244
245 (status, Json(body)).into_response()
246 }
247}
248
249impl From<elif_core::app_config::ConfigError> for HttpError {
251 fn from(err: elif_core::app_config::ConfigError) -> Self {
252 HttpError::ConfigError {
253 message: err.to_string()
254 }
255 }
256}
257
258impl From<std::io::Error> for HttpError {
260 fn from(err: std::io::Error) -> Self {
261 HttpError::InternalError {
262 message: format!("IO error: {}", err)
263 }
264 }
265}
266
267impl From<hyper::Error> for HttpError {
269 fn from(err: hyper::Error) -> Self {
270 HttpError::InternalError {
271 message: format!("Hyper error: {}", err)
272 }
273 }
274}
275
276impl From<elif_orm::ModelError> for HttpError {
278 fn from(err: elif_orm::ModelError) -> Self {
279 match err {
280 elif_orm::ModelError::Database(msg) => HttpError::DatabaseError { message: msg },
281 elif_orm::ModelError::Validation(msg) => HttpError::ValidationError { message: msg },
282 elif_orm::ModelError::NotFound(resource) => HttpError::NotFound { resource },
283 elif_orm::ModelError::Serialization(msg) => HttpError::InternalError {
284 message: format!("Serialization error: {}", msg)
285 },
286 _ => HttpError::InternalError {
287 message: format!("ORM error: {}", err)
288 },
289 }
290 }
291}
292
293impl From<serde_json::Error> for HttpError {
295 fn from(err: serde_json::Error) -> Self {
296 HttpError::InternalError {
297 message: format!("JSON serialization error: {}", err)
298 }
299 }
300}
301
302impl From<sqlx::Error> for HttpError {
304 fn from(err: sqlx::Error) -> Self {
305 match err {
306 sqlx::Error::RowNotFound => HttpError::NotFound {
307 resource: "Resource not found in database".to_string()
308 },
309 sqlx::Error::Database(db_err) => {
310 let err_msg = db_err.message();
312 if err_msg.contains("unique constraint") || err_msg.contains("duplicate key") {
313 HttpError::Conflict {
314 message: "Resource already exists".to_string()
315 }
316 } else {
317 HttpError::DatabaseError {
318 message: format!("Database error: {}", err_msg)
319 }
320 }
321 },
322 _ => HttpError::DatabaseError {
323 message: format!("Database operation failed: {}", err)
324 },
325 }
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_error_creation() {
335 let error = HttpError::startup("Failed to bind to port");
336 assert!(matches!(error, HttpError::StartupFailed { .. }));
337 assert_eq!(error.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
338 assert_eq!(error.error_code(), "SERVER_STARTUP_FAILED");
339 }
340
341 #[test]
342 fn test_error_status_codes() {
343 assert_eq!(HttpError::bad_request("test").status_code(), StatusCode::BAD_REQUEST);
344 assert_eq!(HttpError::RequestTimeout.status_code(), StatusCode::REQUEST_TIMEOUT);
345 assert_eq!(
346 HttpError::RequestTooLarge { size: 100, limit: 50 }.status_code(),
347 StatusCode::PAYLOAD_TOO_LARGE
348 );
349 assert_eq!(
350 HttpError::health_check("Database unavailable").status_code(),
351 StatusCode::SERVICE_UNAVAILABLE
352 );
353 }
354
355 #[test]
356 fn test_error_codes() {
357 assert_eq!(HttpError::bad_request("test").error_code(), "BAD_REQUEST");
358 assert_eq!(HttpError::RequestTimeout.error_code(), "REQUEST_TIMEOUT");
359 assert_eq!(HttpError::internal("test").error_code(), "INTERNAL_ERROR");
360 }
361
362 #[test]
363 fn test_config_error_conversion() {
364 let config_error = elif_core::app_config::ConfigError::MissingEnvVar {
365 var: "TEST_VAR".to_string(),
366 };
367 let http_error = HttpError::from(config_error);
368 assert!(matches!(http_error, HttpError::ConfigError { .. }));
369 }
370
371 #[test]
372 fn test_io_error_conversion() {
373 let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
374 let http_error = HttpError::from(io_error);
375 assert!(matches!(http_error, HttpError::InternalError { .. }));
376 }
377}