dioxus_fullstack_core/
httperror.rs

1use axum_core::response::IntoResponse;
2use http::StatusCode;
3use std::fmt;
4
5/// An error type that wraps an HTTP status code and optional message.
6#[derive(Debug, Clone, PartialEq)]
7pub struct HttpError {
8    pub status: StatusCode,
9    pub message: Option<String>,
10}
11
12impl HttpError {
13    pub fn new<M: Into<String>>(status: StatusCode, message: M) -> Self {
14        HttpError {
15            status,
16            message: Some(message.into()),
17        }
18    }
19
20    pub fn err<T>(status: StatusCode, message: impl Into<String>) -> Result<T, Self> {
21        Err(HttpError::new(status, message))
22    }
23
24    // --- 4xx Client Errors ---
25    pub fn bad_request<T>(message: impl Into<String>) -> Result<T, Self> {
26        Self::err(StatusCode::BAD_REQUEST, message)
27    }
28    pub fn unauthorized<T>(message: impl Into<String>) -> Result<T, Self> {
29        Self::err(StatusCode::UNAUTHORIZED, message)
30    }
31    pub fn payment_required<T>(message: impl Into<String>) -> Result<T, Self> {
32        Self::err(StatusCode::PAYMENT_REQUIRED, message)
33    }
34    pub fn forbidden<T>(message: impl Into<String>) -> Result<T, Self> {
35        Self::err(StatusCode::FORBIDDEN, message)
36    }
37    pub fn not_found<T>(message: impl Into<String>) -> Result<T, Self> {
38        Self::err(StatusCode::NOT_FOUND, message)
39    }
40    pub fn method_not_allowed<T>(message: impl Into<String>) -> Result<T, Self> {
41        Self::err(StatusCode::METHOD_NOT_ALLOWED, message)
42    }
43    pub fn not_acceptable<T>(message: impl Into<String>) -> Result<T, Self> {
44        Self::err(StatusCode::NOT_ACCEPTABLE, message)
45    }
46    pub fn proxy_auth_required<T>(message: impl Into<String>) -> Result<T, Self> {
47        Self::err(StatusCode::PROXY_AUTHENTICATION_REQUIRED, message)
48    }
49    pub fn request_timeout<T>(message: impl Into<String>) -> Result<T, Self> {
50        Self::err(StatusCode::REQUEST_TIMEOUT, message)
51    }
52    pub fn conflict<T>(message: impl Into<String>) -> Result<T, Self> {
53        Self::err(StatusCode::CONFLICT, message)
54    }
55    pub fn gone<T>(message: impl Into<String>) -> Result<T, Self> {
56        Self::err(StatusCode::GONE, message)
57    }
58    pub fn length_required<T>(message: impl Into<String>) -> Result<T, Self> {
59        Self::err(StatusCode::LENGTH_REQUIRED, message)
60    }
61    pub fn precondition_failed<T>(message: impl Into<String>) -> Result<T, Self> {
62        Self::err(StatusCode::PRECONDITION_FAILED, message)
63    }
64    pub fn payload_too_large<T>(message: impl Into<String>) -> Result<T, Self> {
65        Self::err(StatusCode::PAYLOAD_TOO_LARGE, message)
66    }
67    pub fn uri_too_long<T>(message: impl Into<String>) -> Result<T, Self> {
68        Self::err(StatusCode::URI_TOO_LONG, message)
69    }
70    pub fn unsupported_media_type<T>(message: impl Into<String>) -> Result<T, Self> {
71        Self::err(StatusCode::UNSUPPORTED_MEDIA_TYPE, message)
72    }
73    pub fn im_a_teapot<T>(message: impl Into<String>) -> Result<T, Self> {
74        Self::err(StatusCode::IM_A_TEAPOT, message)
75    }
76    pub fn too_many_requests<T>(message: impl Into<String>) -> Result<T, Self> {
77        Self::err(StatusCode::TOO_MANY_REQUESTS, message)
78    }
79
80    // --- 5xx Server Errors ---
81    pub fn internal_server_error<T>(message: impl Into<String>) -> Result<T, Self> {
82        Self::err(StatusCode::INTERNAL_SERVER_ERROR, message)
83    }
84    pub fn not_implemented<T>(message: impl Into<String>) -> Result<T, Self> {
85        Self::err(StatusCode::NOT_IMPLEMENTED, message)
86    }
87    pub fn bad_gateway<T>(message: impl Into<String>) -> Result<T, Self> {
88        Self::err(StatusCode::BAD_GATEWAY, message)
89    }
90    pub fn service_unavailable<T>(message: impl Into<String>) -> Result<T, Self> {
91        Self::err(StatusCode::SERVICE_UNAVAILABLE, message)
92    }
93    pub fn gateway_timeout<T>(message: impl Into<String>) -> Result<T, Self> {
94        Self::err(StatusCode::GATEWAY_TIMEOUT, message)
95    }
96    pub fn http_version_not_supported<T>(message: impl Into<String>) -> Result<T, Self> {
97        Self::err(StatusCode::HTTP_VERSION_NOT_SUPPORTED, message)
98    }
99
100    // --- 2xx/3xx (rare, but for completeness) ---
101    pub fn ok<T>(message: impl Into<String>) -> Result<T, Self> {
102        Self::err(StatusCode::OK, message)
103    }
104    pub fn created<T>(message: impl Into<String>) -> Result<T, Self> {
105        Self::err(StatusCode::CREATED, message)
106    }
107    pub fn accepted<T>(message: impl Into<String>) -> Result<T, Self> {
108        Self::err(StatusCode::ACCEPTED, message)
109    }
110    pub fn moved_permanently<T>(message: impl Into<String>) -> Result<T, Self> {
111        Self::err(StatusCode::MOVED_PERMANENTLY, message)
112    }
113    pub fn found<T>(message: impl Into<String>) -> Result<T, Self> {
114        Self::err(StatusCode::FOUND, message)
115    }
116    pub fn see_other<T>(message: impl Into<String>) -> Result<T, Self> {
117        Self::err(StatusCode::SEE_OTHER, message)
118    }
119    pub fn not_modified<T>(message: impl Into<String>) -> Result<T, Self> {
120        Self::err(StatusCode::NOT_MODIFIED, message)
121    }
122}
123
124impl fmt::Display for HttpError {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        match &self.message {
127            Some(msg) => write!(f, "{}: {}", self.status, msg),
128            None => write!(f, "{}", self.status),
129        }
130    }
131}
132
133impl std::error::Error for HttpError {}
134
135/// Trait to convert errors into HttpError with a given status code.
136pub trait OrHttpError<T, M>: Sized {
137    fn or_http_error(self, status: StatusCode, message: impl Into<String>) -> Result<T, HttpError>;
138
139    // --- Most common user-facing status codes ---
140    fn or_bad_request(self, message: impl Into<String>) -> Result<T, HttpError> {
141        self.or_http_error(StatusCode::BAD_REQUEST, message)
142    }
143    fn or_unauthorized(self, message: impl Into<String>) -> Result<T, HttpError> {
144        self.or_http_error(StatusCode::UNAUTHORIZED, message)
145    }
146    fn or_forbidden(self, message: impl Into<String>) -> Result<T, HttpError> {
147        self.or_http_error(StatusCode::FORBIDDEN, message)
148    }
149    fn or_not_found(self, message: impl Into<String>) -> Result<T, HttpError> {
150        self.or_http_error(StatusCode::NOT_FOUND, message)
151    }
152    fn or_internal_server_error(self, message: impl Into<String>) -> Result<T, HttpError> {
153        self.or_http_error(StatusCode::INTERNAL_SERVER_ERROR, message)
154    }
155}
156
157impl<T, E> OrHttpError<T, ()> for Result<T, E>
158where
159    E: std::error::Error + Send + Sync + 'static,
160{
161    fn or_http_error(self, status: StatusCode, message: impl Into<String>) -> Result<T, HttpError> {
162        self.map_err(|_| HttpError {
163            status,
164            message: Some(message.into()),
165        })
166    }
167}
168
169impl<T> OrHttpError<T, ()> for Option<T> {
170    fn or_http_error(self, status: StatusCode, message: impl Into<String>) -> Result<T, HttpError> {
171        self.ok_or_else(|| HttpError {
172            status,
173            message: Some(message.into()),
174        })
175    }
176}
177
178impl OrHttpError<(), ()> for bool {
179    fn or_http_error(
180        self,
181        status: StatusCode,
182        message: impl Into<String>,
183    ) -> Result<(), HttpError> {
184        if self {
185            Ok(())
186        } else {
187            Err(HttpError {
188                status,
189                message: Some(message.into()),
190            })
191        }
192    }
193}
194
195pub struct AnyhowMarker;
196impl<T> OrHttpError<T, AnyhowMarker> for Result<T, anyhow::Error> {
197    fn or_http_error(self, status: StatusCode, message: impl Into<String>) -> Result<T, HttpError> {
198        self.map_err(|_| HttpError {
199            status,
200            message: Some(message.into()),
201        })
202    }
203}
204
205impl IntoResponse for HttpError {
206    fn into_response(self) -> axum_core::response::Response {
207        let body = match &self.message {
208            Some(msg) => msg.clone(),
209            None => self
210                .status
211                .canonical_reason()
212                .unwrap_or("Unknown error")
213                .to_string(),
214        };
215        (self.status, body).into_response()
216    }
217}