1use axum::{
2 http::StatusCode,
3 response::{IntoResponse, Response},
4 Json,
5};
6use serde::Serialize;
7use serde_json::Value;
8
9use crate::ValidationErrors;
10
11#[derive(Serialize)]
22struct ErrorBody {
23 #[serde(rename = "statusCode")]
24 status_code: u16,
25 error: String,
26 code: &'static str,
27 message: String,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 details: Option<Value>,
30 #[serde(rename = "requestId", skip_serializing_if = "Option::is_none")]
31 request_id: Option<String>,
32}
33
34#[derive(Debug, Clone)]
39pub struct HttpException {
40 pub status: StatusCode,
41 pub code: &'static str,
42 pub message: String,
43 pub details: Option<Value>,
44 pub request_id: Option<String>,
45}
46
47impl HttpException {
48 pub fn new(status: StatusCode, code: &'static str, message: impl Into<String>) -> Self {
52 Self {
53 status,
54 code,
55 message: message.into(),
56 details: None,
57 request_id: None,
58 }
59 }
60
61 pub fn with_details(
62 status: StatusCode,
63 code: &'static str,
64 message: impl Into<String>,
65 details: Value,
66 ) -> Self {
67 Self {
68 status,
69 code,
70 message: message.into(),
71 details: Some(details),
72 request_id: None,
73 }
74 }
75
76 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
77 self.request_id = Some(request_id.into());
78 self
79 }
80
81 pub fn with_optional_request_id(mut self, request_id: Option<String>) -> Self {
82 self.request_id = request_id;
83 self
84 }
85
86 pub fn bad_request(message: impl Into<String>) -> Self {
90 Self::new(StatusCode::BAD_REQUEST, "bad_request", message)
91 }
92
93 pub fn bad_request_validation(errors: ValidationErrors) -> Self {
94 let message = "Validation failed".to_string();
95 let details = serde_json::to_value(errors).unwrap_or(Value::Null);
96 Self::with_details(
97 StatusCode::BAD_REQUEST,
98 "validation_failed",
99 message,
100 details,
101 )
102 }
103
104 pub fn unauthorized(message: impl Into<String>) -> Self {
105 Self::new(StatusCode::UNAUTHORIZED, "unauthorized", message)
106 }
107
108 pub fn forbidden(message: impl Into<String>) -> Self {
109 Self::new(StatusCode::FORBIDDEN, "forbidden", message)
110 }
111
112 pub fn not_found(message: impl Into<String>) -> Self {
113 Self::new(StatusCode::NOT_FOUND, "not_found", message)
114 }
115
116 pub fn internal_server_error(message: impl Into<String>) -> Self {
117 Self::new(
118 StatusCode::INTERNAL_SERVER_ERROR,
119 "internal_server_error",
120 message,
121 )
122 }
123}
124
125impl IntoResponse for HttpException {
133 fn into_response(self) -> Response {
134 let error_name = self
135 .status
136 .canonical_reason()
137 .unwrap_or("Error")
138 .to_string();
139
140 let body = ErrorBody {
141 status_code: self.status.as_u16(),
142 error: error_name,
143 code: self.code,
144 message: self.message,
145 details: self.details,
146 request_id: self.request_id,
147 };
148
149 (self.status, Json(body)).into_response()
150 }
151}