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
use crate::couch::CouchError;
use oas_common::{DecodingError, EncodingError, ValidationError};
use okapi::openapi3::Responses;
use rocket::http::Status;
use rocket::response::Responder;
use rocket::{response, response::content, Request};
use rocket_okapi::gen::OpenApiGenerator;
use rocket_okapi::response::OpenApiResponderInner;
use rocket_okapi::util::add_schema_response;
use schemars::JsonSchema;
use serde::Serialize;

use thiserror::Error;

pub type Result<T> = std::result::Result<rocket::serde::json::Json<T>, AppError>;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("{0}")]
    DecodingError(#[from] DecodingError),
    #[error("{0}")]
    EncodingError(#[from] EncodingError),
    #[error("{0}")]
    Couch(#[from] CouchError),
    #[error("{0}")]
    Serde(#[from] serde_json::Error),
    #[error("{0}")]
    Other(String),
    #[error("{0}")]
    Elastic(#[from] elasticsearch::Error),
    #[error("HTTP error: {0} {1}")]
    Http(Status, String),
    #[error("Validation error: {0}")]
    ValidationError(ValidationError),
    #[error("Unauthorized")]
    Unauthorized,
}

impl From<anyhow::Error> for AppError {
    fn from(err: anyhow::Error) -> Self {
        Self::Other(format!("{}", err))
    }
}

impl<'r> Responder<'r, 'static> for AppError {
    fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
        log::debug!("{:?}", self);
        let code = match &self {
            AppError::Couch(err) => map_u16_status(err.status_code()),
            AppError::Http(code, _) => *code,
            AppError::EncodingError(_) => Status::BadRequest,
            AppError::ValidationError(_) => Status::UnprocessableEntity,
            AppError::Elastic(err) => map_u16_status(err.status_code().map(|code| code.as_u16())),
            AppError::Unauthorized => Status::Unauthorized,
            _ => Status::InternalServerError,
        };

        let message = match &self {
            AppError::Http(_code, message) => message.clone(),
            _ => format!("{}", self),
        };

        let response = ErrorResponse { error: message };

        // let json = json!({ "error": message });
        let json_string = serde_json::to_string(&response).unwrap();
        let res = content::Json(json_string).respond_to(req);

        // Handle authentication error: Add WWW-Authenticate header.
        match res {
            Err(res) => Err(res),
            Ok(mut res) => {
                res.set_status(code);
                if let Self::Unauthorized = self {
                    // TODO: This is nice as it makes the API accessible in regular browsers.
                    // However, this also makes logout basically "not work" because browsers
                    // remember the basic auth details by default and re-add them.
                    let header_value = format!(
                        r#"Basic realm="{}", charset="UTF-8""#,
                        "Please enter user username and password"
                    );
                    let header = rocket::http::Header::new(
                        http::header::WWW_AUTHENTICATE.as_str(),
                        header_value,
                    );
                    res.set_header(header);
                }
                Ok(res)
            }
        }
        // let res = Custom(code, content::Json(json_string)).respond_to(req);
    }
}

fn map_u16_status(status: Option<u16>) -> Status {
    status
        .map(|code| Status::from_code(code).unwrap())
        .unwrap_or(Status::InternalServerError)
}

#[derive(Serialize, JsonSchema, Debug, Default)]
struct ErrorResponse {
    error: String,
}

impl OpenApiResponderInner for AppError {
    fn responses(gen: &mut OpenApiGenerator) -> rocket_okapi::Result<Responses> {
        let mut responses = Responses::default();
        let schema = gen.json_schema::<ErrorResponse>();
        // TODO: Find out how different codes are displayed.
        add_schema_response(&mut responses, 500, "application/json", schema)?;
        Ok(responses)
    }
}