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