1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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 {
    // Skip `status_code` field to allow to derive Apiv2Schema without wrapping StatusCode type;
    // document and serialize field `status` instead.
    // Keep them private to assure consistency
    #[serde(skip)]
    status_code: StatusCode,
    status: u16,
    /// Generic type common to all services
    pub r#type: JsonErrorType,
    /// Message to be displayed to the user
    pub message: String,
    /// Error representation for tracing exact place in the code where error occurred
    pub debug: Option<String>,
    /// Detailed reason of the error
    pub reason: String,
    /// Any additional data, needs to be used when presenting error to the user (f. ex. validation errors)
    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 {
    // Starters
    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)
    }

    // Modifiers
    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
    }

    // Produce ready error
    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
    }
}

/// Middleware for converting plain actix errors into JSON ones with JsonError schema
pub fn default_error_handler<B: MessageBody + 'static>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
    // Disassemble service response
    let (req, mut response) = res.into_parts();

    let json_content_type  = header::HeaderValue::from_static("application/json; charset=utf-8");

    // Rewrite response only if it is an error, but not JSON already
    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);

        // Happily error message can be taken from "special place" in response struct,
        // not necessarily from the body which requires waiting.
        // If error message is not provided, take it from status code.
        // TODO: Maybe sometimes it would be better to place this in 'reason' but how to distinguish when?
        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,
        };

        // Overwrite response content-type
        response.headers_mut().insert(header::CONTENT_TYPE, json_content_type);

        // Overwrite response body
        response.set_body(serde_json::to_string(&err).unwrap()).map_into_boxed_body()
    } else {
        // Leave response intact
        response.map_into_boxed_body()
    };

    let res = ServiceResponse::new(req, res).map_into_right_body();
    Ok(ErrorHandlerResponse::Response(res))
}