use actix_http::body::MessageBody;
use actix_web::{
dev::ServiceResponse,
http::{header, StatusCode},
HttpResponse,
middleware::ErrorHandlerResponse,
ResponseError,
Result,
};
use derive_more::Display;
use serde::Serialize;
use std::fmt::{Debug, Display};
pub use serwus_derive::ResponseFromBuilder;
#[derive(Debug, Display, Serialize)]
#[cfg_attr(feature = "swagger", derive(paperclip::actix::Apiv2Schema))]
pub enum JsonErrorType {
BadRequest,
NotFound,
Internal,
Other,
Database,
ValidationFail,
InvalidParams,
Custom(String),
}
impl From<StatusCode> for JsonErrorType {
fn from(value: StatusCode) -> Self {
if value == StatusCode::NOT_FOUND {
Self::NotFound
} else if value.is_client_error() {
Self::BadRequest
} else if value.is_server_error() {
Self::Internal
} else {
Self::Other
}
}
}
#[derive(Debug, Display, Serialize)]
#[cfg_attr(feature = "swagger", derive(paperclip::actix::Apiv2Schema))]
#[display(fmt = "{} ({}) {}", status_code, r#type, message)]
pub struct JsonError {
#[serde(skip)]
status_code: StatusCode,
status: u16,
pub r#type: JsonErrorType,
pub message: String,
pub debug: Option<String>,
pub reason: String,
pub data: Option<serde_json::Value>,
}
pub const GENERIC_MESSAGE: &str = "Something went wrong. Try again later";
pub const GENERIC_REASON: &str = "Unknown";
pub struct ErrorBuilder {
inner: JsonError
}
impl ErrorBuilder {
pub fn new(status_code: StatusCode, r#type: JsonErrorType, reason: impl Display) -> ErrorBuilder {
let reason = reason.to_string();
Self {
inner: JsonError {
status: status_code.as_u16(),
status_code,
r#type,
message: GENERIC_MESSAGE.to_string(),
debug: None,
reason,
data: None,
}
}
}
pub fn internal(reason: impl Display) -> Self {
let status_code = StatusCode::INTERNAL_SERVER_ERROR;
Self::new(status_code, JsonErrorType::Internal, reason)
}
pub fn database(reason: impl Display) -> Self {
let status_code = StatusCode::INTERNAL_SERVER_ERROR;
Self::new(status_code, JsonErrorType::Database, reason)
}
pub fn validation_fail(reason: impl Display) -> Self {
let status_code = StatusCode::UNPROCESSABLE_ENTITY;
Self::new(status_code, JsonErrorType::ValidationFail, reason)
}
pub fn custom(sub_type: impl Display, reason: impl Display) -> Self {
let status_code = StatusCode::INTERNAL_SERVER_ERROR;
Self::new(status_code, JsonErrorType::Custom(sub_type.to_string()), reason)
}
pub fn status(mut self, status_code: StatusCode) -> Self {
self.inner.status = status_code.as_u16();
self.inner.status_code = status_code;
self
}
pub fn message(mut self, message: impl Display) -> Self {
self.inner.message = message.to_string();
self
}
pub fn r#type(mut self, r#type: JsonErrorType) -> Self {
self.inner.r#type = r#type;
self
}
pub fn debug(mut self, debug: impl Debug) -> Self {
self.inner.debug = Some(format!("{:?}", debug));
self
}
pub fn data(mut self, data: impl Serialize) -> Self {
let data = serde_json::to_value(&data)
.or_else(|err| {
let err_str = err.to_string();
log::error!("Error serializing error: {}", err_str);
Ok::<_, ()>(serde_json::Value::String(format!("Error serializing error: {}", err_str)))
})
.ok();
self.inner.data = data;
self
}
pub fn finish(self) -> JsonError {
self.inner
}
}
impl ResponseError for JsonError {
fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
if self.status_code > StatusCode::INTERNAL_SERVER_ERROR {
log::error!("{} (reason: {})", self, self.reason);
}
HttpResponse::build(self.status_code)
.content_type("application/json; charset=utf-8")
.json(self)
}
fn status_code(&self) -> StatusCode {
self.status_code
}
}
pub fn default_error_handler<B: MessageBody + 'static>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
let (req, mut response) = res.into_parts();
let json_content_type = header::HeaderValue::from_static("application/json; charset=utf-8");
let res = if !response.status().is_success() && response.headers().get(header::CONTENT_TYPE) != Some(&json_content_type) {
let status_code = response.status();
let r#type = JsonErrorType::from(status_code);
let message = response.error()
.map(|err| err.to_string())
.unwrap_or_else(|| status_code.to_string());
let debug = "default_error_handler".to_string();
let reason = "".to_string();
let err = JsonError {
status: status_code.as_u16(),
status_code,
r#type,
message,
debug: Some(debug),
reason,
data: None,
};
response.headers_mut().insert(header::CONTENT_TYPE, json_content_type);
response.set_body(serde_json::to_string(&err).unwrap()).map_into_boxed_body()
} else {
response.map_into_boxed_body()
};
let res = ServiceResponse::new(req, res).map_into_right_body();
Ok(ErrorHandlerResponse::Response(res))
}