lmrc_http_common/
error.rs

1//! Common error types for HTTP services
2
3use axum::{
4    http::StatusCode,
5    response::{IntoResponse, Response},
6    Json,
7};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11/// Standard error response format
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ErrorResponse {
14    /// Error code/type
15    pub error: String,
16    /// Human-readable error message
17    pub message: String,
18    /// Optional additional details
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub details: Option<serde_json::Value>,
21}
22
23impl ErrorResponse {
24    pub fn new(error: impl Into<String>, message: impl Into<String>) -> Self {
25        Self {
26            error: error.into(),
27            message: message.into(),
28            details: None,
29        }
30    }
31
32    pub fn with_details(mut self, details: serde_json::Value) -> Self {
33        self.details = Some(details);
34        self
35    }
36}
37
38/// Common HTTP application errors
39#[derive(Debug, Error)]
40pub enum HttpError {
41    /// Bad request (400)
42    #[error("Bad request: {0}")]
43    BadRequest(String),
44
45    /// Unauthorized (401)
46    #[error("Unauthorized: {0}")]
47    Unauthorized(String),
48
49    /// Forbidden (403)
50    #[error("Forbidden: {0}")]
51    Forbidden(String),
52
53    /// Not found (404)
54    #[error("Not found: {0}")]
55    NotFound(String),
56
57    /// Conflict (409)
58    #[error("Conflict: {0}")]
59    Conflict(String),
60
61    /// Unprocessable entity (422)
62    #[error("Validation failed: {0}")]
63    ValidationError(String),
64
65    /// Internal server error (500)
66    #[error("Internal server error: {0}")]
67    InternalServer(String),
68
69    /// Service unavailable (503)
70    #[error("Service unavailable: {0}")]
71    ServiceUnavailable(String),
72
73    /// Database error
74    #[error("Database error: {0}")]
75    Database(String),
76
77    /// External service error
78    #[error("External service error: {0}")]
79    ExternalService(String),
80
81    /// Custom error with status code
82    #[error("{message}")]
83    Custom {
84        status: StatusCode,
85        error_code: String,
86        message: String,
87    },
88}
89
90impl HttpError {
91    /// Get the HTTP status code for this error
92    pub fn status_code(&self) -> StatusCode {
93        match self {
94            HttpError::BadRequest(_) => StatusCode::BAD_REQUEST,
95            HttpError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
96            HttpError::Forbidden(_) => StatusCode::FORBIDDEN,
97            HttpError::NotFound(_) => StatusCode::NOT_FOUND,
98            HttpError::Conflict(_) => StatusCode::CONFLICT,
99            HttpError::ValidationError(_) => StatusCode::UNPROCESSABLE_ENTITY,
100            HttpError::InternalServer(_) => StatusCode::INTERNAL_SERVER_ERROR,
101            HttpError::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
102            HttpError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
103            HttpError::ExternalService(_) => StatusCode::BAD_GATEWAY,
104            HttpError::Custom { status, .. } => *status,
105        }
106    }
107
108    /// Get the error code string
109    pub fn error_code(&self) -> String {
110        match self {
111            HttpError::BadRequest(_) => "BAD_REQUEST".to_string(),
112            HttpError::Unauthorized(_) => "UNAUTHORIZED".to_string(),
113            HttpError::Forbidden(_) => "FORBIDDEN".to_string(),
114            HttpError::NotFound(_) => "NOT_FOUND".to_string(),
115            HttpError::Conflict(_) => "CONFLICT".to_string(),
116            HttpError::ValidationError(_) => "VALIDATION_ERROR".to_string(),
117            HttpError::InternalServer(_) => "INTERNAL_SERVER_ERROR".to_string(),
118            HttpError::ServiceUnavailable(_) => "SERVICE_UNAVAILABLE".to_string(),
119            HttpError::Database(_) => "DATABASE_ERROR".to_string(),
120            HttpError::ExternalService(_) => "EXTERNAL_SERVICE_ERROR".to_string(),
121            HttpError::Custom { error_code, .. } => error_code.clone(),
122        }
123    }
124
125    /// Create a custom error with specific status code
126    pub fn custom(
127        status: StatusCode,
128        error_code: impl Into<String>,
129        message: impl Into<String>,
130    ) -> Self {
131        HttpError::Custom {
132            status,
133            error_code: error_code.into(),
134            message: message.into(),
135        }
136    }
137}
138
139impl IntoResponse for HttpError {
140    fn into_response(self) -> Response {
141        let status = self.status_code();
142        let error_code = self.error_code();
143        let message = self.to_string();
144
145        let body = ErrorResponse::new(error_code, message);
146
147        (status, Json(body)).into_response()
148    }
149}
150
151/// Convenience type for Result with HttpError
152pub type HttpResult<T> = Result<T, HttpError>;
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_error_status_codes() {
160        assert_eq!(
161            HttpError::BadRequest("test".into()).status_code(),
162            StatusCode::BAD_REQUEST
163        );
164        assert_eq!(
165            HttpError::Unauthorized("test".into()).status_code(),
166            StatusCode::UNAUTHORIZED
167        );
168        assert_eq!(
169            HttpError::NotFound("test".into()).status_code(),
170            StatusCode::NOT_FOUND
171        );
172    }
173
174    #[test]
175    fn test_custom_error() {
176        let err = HttpError::custom(StatusCode::IM_A_TEAPOT, "TEAPOT", "I'm a teapot");
177        assert_eq!(err.status_code(), StatusCode::IM_A_TEAPOT);
178        assert_eq!(err.error_code(), "TEAPOT");
179    }
180}