Skip to main content

foxtive_ntex/
error.rs

1use crate::error::helpers::make_http_error_response;
2use crate::http::response::anyhow::helpers::make_status_code;
3use foxtive::Error;
4use foxtive::prelude::AppMessage;
5#[cfg(feature = "multipart")]
6use foxtive_ntex_multipart::{ErrorMessage as MultipartErrorMessage, MultipartError};
7use ntex::http::StatusCode;
8use ntex::http::error::PayloadError;
9use ntex::web::error::{BlockingError, JsonError};
10use ntex::web::{HttpRequest, HttpResponse, WebResponseError};
11use std::string::FromUtf8Error;
12use thiserror::Error;
13use tokio::task::JoinError;
14
15#[derive(Error, Debug)]
16pub enum HttpError {
17    #[error("{0}")]
18    Std(Box<dyn std::error::Error + Send + Sync + 'static>),
19    #[error("{0}")]
20    AppError(#[from] Error),
21    #[error("{0}")]
22    AppMessage(#[from] AppMessage),
23    #[error("Payload Error: {0}")]
24    PayloadError(#[from] PayloadError),
25    #[error("Join Error: {0}")]
26    JoinError(#[from] JoinError),
27    #[error("Utf8 Error: {0}")]
28    Utf8Error(#[from] FromUtf8Error),
29    #[error("Json Error: {0}")]
30    JsonError(#[from] JsonError),
31    #[cfg(feature = "validator")]
32    #[error("Validation Error: {0}")]
33    ValidationError(#[from] validator::ValidationErrors),
34    #[cfg(feature = "multipart")]
35    #[error("Multipart Error: {0}")]
36    MultipartError(#[from] MultipartError),
37}
38
39impl HttpError {
40    pub fn into_app_error(self) -> foxtive::Error {
41        foxtive::Error::from(self)
42    }
43}
44
45impl From<Box<dyn std::error::Error + Send + Sync>> for HttpError {
46    fn from(error: Box<dyn std::error::Error + Send + Sync>) -> Self {
47        HttpError::Std(error)
48    }
49}
50
51impl From<BlockingError<Error>> for HttpError {
52    fn from(value: BlockingError<Error>) -> Self {
53        match value {
54            BlockingError::Error(e) => HttpError::AppError(e),
55            BlockingError::Canceled => HttpError::AppMessage(AppMessage::InternalServerError),
56        }
57    }
58}
59
60impl WebResponseError for HttpError {
61    fn status_code(&self) -> StatusCode {
62        match self {
63            HttpError::AppMessage(m) => m.status_code(),
64            HttpError::AppError(e) => make_status_code(e),
65            #[cfg(feature = "validator")]
66            HttpError::ValidationError(_) => StatusCode::BAD_REQUEST,
67            HttpError::PayloadError(_) | HttpError::JsonError(_) => StatusCode::BAD_REQUEST,
68            #[cfg(feature = "multipart")]
69            HttpError::MultipartError(err) => match err {
70                MultipartError::ValidationError(err) => match err.error {
71                    MultipartErrorMessage::InvalidFileExtension(_, _, _)
72                    | MultipartErrorMessage::InvalidContentType(_, _, _) => {
73                        StatusCode::UNSUPPORTED_MEDIA_TYPE
74                    }
75                    _ => StatusCode::BAD_REQUEST,
76                },
77                _ => StatusCode::BAD_REQUEST,
78            },
79            _ => StatusCode::INTERNAL_SERVER_ERROR,
80        }
81    }
82
83    fn error_response(&self, _: &HttpRequest) -> HttpResponse {
84        make_http_error_response(self)
85    }
86}
87
88pub(crate) mod helpers {
89    use crate::enums::ResponseCode;
90    use crate::http::HttpError;
91    use crate::http::responder::Responder;
92    use crate::http::response::anyhow::helpers::make_response;
93    use foxtive::prelude::AppMessage;
94    #[cfg(feature = "multipart")]
95    use foxtive_ntex_multipart::MultipartError;
96    use ntex::web::HttpResponse;
97    use tracing::error;
98
99    pub(crate) fn make_http_error_response(err: &HttpError) -> HttpResponse {
100        match err {
101            HttpError::AppMessage(m) => make_response(&m.clone().ae()),
102            HttpError::AppError(e) => make_response(e),
103            #[cfg(feature = "validator")]
104            HttpError::ValidationError(e) => {
105                Responder::send_msg(e.errors(), ResponseCode::BadRequest, "Validation Error")
106            }
107            HttpError::PayloadError(e) => {
108                error!("Payload Error: {e}");
109                Responder::send_msg(e.to_string(), ResponseCode::BadRequest, "Payload Error")
110            }
111            HttpError::JsonError(e) => {
112                error!("Json Error: {e}");
113                Responder::send_msg(
114                    e.to_string(),
115                    ResponseCode::BadRequest,
116                    "Json Payload Error",
117                )
118            }
119            #[cfg(feature = "multipart")]
120            HttpError::MultipartError(err) => match err {
121                MultipartError::ValidationError(val) => {
122                    let msg = err.to_string();
123                    Responder::send_msg(
124                        serde_json::json!({"field": val.name, "error": msg}),
125                        ResponseCode::BadRequest,
126                        &msg,
127                    )
128                }
129                _ => {
130                    let msg = err.to_string();
131                    Responder::send_msg(
132                        serde_json::json!({"message": msg}),
133                        ResponseCode::BadRequest,
134                        &msg,
135                    )
136                }
137            },
138            _ => {
139                error!("Error: {err}");
140                make_response(&foxtive::Error::from(AppMessage::InternalServerError))
141            }
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use foxtive::Error;
150
151    #[test]
152    fn test_app_error() {
153        let error = HttpError::AppError(Error::from(AppMessage::InternalServerError));
154        let app_error = make_http_error_response(&error);
155        assert_eq!(app_error.status(), 500);
156    }
157
158    #[test]
159    fn test_app_message() {
160        let error = HttpError::AppMessage(AppMessage::InternalServerError);
161        let app_error = make_http_error_response(&error);
162        assert_eq!(app_error.status(), 500);
163    }
164
165    #[test]
166    fn test_std_error() {
167        #[allow(clippy::io_other_error)]
168        let error = HttpError::Std(Box::new(std::io::Error::new(
169            std::io::ErrorKind::Other,
170            "Test",
171        )));
172        let app_error = make_http_error_response(&error);
173        assert_eq!(app_error.status(), 500);
174    }
175
176    #[test]
177    fn test_payload_error() {
178        let error = HttpError::PayloadError(PayloadError::Overflow);
179        let app_error = make_http_error_response(&error);
180        assert_eq!(app_error.status(), 400);
181    }
182
183    #[cfg(feature = "validator")]
184    #[test]
185    fn test_validation_error() {
186        let error = HttpError::ValidationError(validator::ValidationErrors::new());
187        let app_error = make_http_error_response(&error);
188        assert_eq!(app_error.status(), 400);
189    }
190
191    #[cfg(feature = "multipart")]
192    #[test]
193    fn test_multipart_error() {
194        use foxtive_ntex_multipart::InputError;
195
196        let error = HttpError::MultipartError(MultipartError::ValidationError(InputError {
197            error: MultipartErrorMessage::InvalidFileExtension(
198                "image".to_string(),
199                "invalid ext".to_string(),
200                Some("mp4".to_string()),
201            ),
202            name: "image".to_string(),
203        }));
204
205        let app_error = make_http_error_response(&error);
206
207        assert_eq!(app_error.status(), 400);
208    }
209}