lmrc_http_common/
error.rs1use axum::{
4 http::StatusCode,
5 response::{IntoResponse, Response},
6 Json,
7};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ErrorResponse {
14 pub error: String,
16 pub message: String,
18 #[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#[derive(Debug, Error)]
40pub enum HttpError {
41 #[error("Bad request: {0}")]
43 BadRequest(String),
44
45 #[error("Unauthorized: {0}")]
47 Unauthorized(String),
48
49 #[error("Forbidden: {0}")]
51 Forbidden(String),
52
53 #[error("Not found: {0}")]
55 NotFound(String),
56
57 #[error("Conflict: {0}")]
59 Conflict(String),
60
61 #[error("Validation failed: {0}")]
63 ValidationError(String),
64
65 #[error("Internal server error: {0}")]
67 InternalServer(String),
68
69 #[error("Service unavailable: {0}")]
71 ServiceUnavailable(String),
72
73 #[error("Database error: {0}")]
75 Database(String),
76
77 #[error("External service error: {0}")]
79 ExternalService(String),
80
81 #[error("{message}")]
83 Custom {
84 status: StatusCode,
85 error_code: String,
86 message: String,
87 },
88}
89
90impl HttpError {
91 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 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 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
151pub 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}