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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
mod error_types;
pub use error_types::*;

use chrono::{DateTime, Local};
use http::header::HeaderValue;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{ser::Serializer, Serialize};
use serde_json::error::Category;
use serde_json::{Map, Value};
use std::{
    error::Error as StdError,
    fmt::{self, Display},
};
use warp::{reject::custom, reply::json, reply::Response, Rejection, Reply};

/// API error type prefix of problems.
/// This URL prefix is currently not published but we assume that in the future.
const ERROR_TYPE_PREFIX: &str = "https://errors.interledger.org/http-api";

/// This struct represents the fields defined in [RFC7807](https://tools.ietf.org/html/rfc7807).
/// The meaning of each field could be found at [Members of a Problem Details Object](https://tools.ietf.org/html/rfc7807#section-3.1) section.
/// ApiError implements Reply so that it could be used for responses.
#[derive(Clone, Debug, Serialize)]
pub struct ApiError {
    /// `type` is a URI which represents an error type. The URI should provide human-readable
    /// documents so that developers can solve the problem easily.
    #[serde(serialize_with = "serialize_type")]
    pub r#type: &'static ProblemType,
    /// `title` is a short, human-readable summary of the type.
    /// SHOULD NOT change from occurrence to occurrence of the problem, except for purposes
    /// of localization.
    pub title: &'static str,
    /// `status` is a HTTP status of the problem.
    #[serde(serialize_with = "serialize_status_code")]
    pub status: http::StatusCode,
    /// `detail` explains the problem in human-readable detail.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub detail: Option<String>,
    /// `instance` is a URI reference that identifies the specific occurrence of the problem.
    /// We should be careful of how we provide the URI because if it provides very detailed
    /// information about the error, it might expose some vulnerabilities of the node.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub instance: Option<String>,
    /// `extension_members` is a Map of JSON values which will be flatly injected into response
    /// JSONs. For example, if we specify `extension_members` like:
    /// ```JSON
    /// "invalid-params": [
    ///     { "name": "Username", "type": "missing" }
    /// ]
    /// ```
    /// then this map is merged into the response JSON and will look like:
    /// ```JSON
    /// {
    ///     "type": "about:blank",
    ///     "title": "Missing Fields Error",
    ///     "status": 400,
    ///     "detail": "foo bar",
    ///     "invalid-params": [
    ///         { "name": "Username", "type": "missing" }
    ///     ]
    /// }
    /// ```
    #[serde(flatten, skip_serializing_if = "Option::is_none")]
    pub extension_members: Option<Map<String, Value>>,
}

#[derive(Clone, Copy, Debug)]
pub enum ProblemType {
    /// `Default` is a [pre-defined value](https://tools.ietf.org/html/rfc7807#section-4.2) which is
    /// going to be serialized as `about:blank`.
    Default,
    /// InterledgerHttpApi is a API specific error type which is going to be serialized like
    /// `https://errors.interledger.org/http-api/foo-bar`. Variant means path, in the example,
    /// `foo-bar` is the path.
    InterledgerHttpApi(&'static str),
}

#[derive(Clone, Copy, Debug)]
pub struct ApiErrorType {
    pub r#type: &'static ProblemType,
    pub title: &'static str,
    pub status: http::StatusCode,
}

// This should be OK because serde serializer MUST be `fn<S>(&T, S)`
#[allow(clippy::trivially_copy_pass_by_ref)]
fn serialize_status_code<S>(status: &http::StatusCode, s: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    s.serialize_u16(status.as_u16())
}

fn serialize_type<S>(r#type: &ProblemType, s: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    match r#type {
        ProblemType::Default => s.serialize_str("about:blank"),
        ProblemType::InterledgerHttpApi(custom_type) => {
            s.serialize_str(&format!("{}/{}", ERROR_TYPE_PREFIX, custom_type))
        }
    }
}

impl ApiError {
    pub fn from_api_error_type(problem_type: &ApiErrorType) -> Self {
        ApiError {
            r#type: problem_type.r#type,
            title: problem_type.title,
            status: problem_type.status,
            detail: None,
            instance: None,
            extension_members: Some(ApiError::merge_default_extension_members(None)),
        }
    }

    // Note that we should basically avoid using the following default errors because
    // we should provide more detailed information for developers
    #[allow(dead_code)]
    pub fn bad_request() -> Self {
        ApiError::from_api_error_type(&DEFAULT_BAD_REQUEST_TYPE)
    }

    pub fn internal_server_error() -> Self {
        ApiError::from_api_error_type(&DEFAULT_INTERNAL_SERVER_ERROR_TYPE)
    }

    pub fn unauthorized() -> Self {
        ApiError::from_api_error_type(&DEFAULT_UNAUTHORIZED_TYPE)
    }

    #[allow(dead_code)]
    pub fn not_found() -> Self {
        ApiError::from_api_error_type(&DEFAULT_NOT_FOUND_TYPE)
    }

    #[allow(dead_code)]
    pub fn method_not_allowed() -> Self {
        ApiError::from_api_error_type(&DEFAULT_METHOD_NOT_ALLOWED_TYPE)
    }

    pub fn account_not_found() -> Self {
        ApiError::from_api_error_type(&ACCOUNT_NOT_FOUND_TYPE)
            .detail("Username was not found.".to_owned())
    }

    #[allow(dead_code)]
    pub fn idempotency_conflict() -> Self {
        ApiError::from_api_error_type(&DEFAULT_IDEMPOTENT_CONFLICT_TYPE)
    }

    pub fn invalid_account_id(invalid_account_id: Option<&str>) -> Self {
        let detail = match invalid_account_id {
            Some(invalid_account_id) => match invalid_account_id.len() {
                0 => "Account ID is empty".to_owned(),
                _ => format!("{} is an invalid account ID", invalid_account_id),
            },
            None => "Invalid string was given as an account ID".to_owned(),
        };
        ApiError::from_api_error_type(&INVALID_ACCOUNT_ID_TYPE).detail(detail)
    }

    pub fn invalid_ilp_packet() -> Self {
        ApiError::from_api_error_type(&INVALID_ILP_PACKET_TYPE)
    }

    pub fn detail<T>(mut self, detail: T) -> Self
    where
        T: Into<String>,
    {
        self.detail = Some(detail.into());
        self
    }

    #[allow(dead_code)]
    pub fn instance<T>(mut self, instance: T) -> Self
    where
        T: Into<String>,
    {
        self.instance = Some(instance.into());
        self
    }

    pub fn extension_members(mut self, extension_members: Option<Map<String, Value>>) -> Self {
        self.extension_members = extension_members;
        self
    }

    fn get_base_extension_members() -> Map<String, Value> {
        // TODO Should implement request wide time
        let datetime: DateTime<Local> = Local::now();
        let mut map = serde_json::Map::new();
        map.insert("datetime".to_owned(), Value::from(datetime.to_rfc3339()));
        map
    }

    fn merge_default_extension_members(
        extension_members: Option<Map<String, Value>>,
    ) -> Map<String, Value> {
        let mut merged_extension_members = ApiError::get_base_extension_members();
        if let Some(map) = extension_members {
            for (k, v) in map {
                merged_extension_members.insert(k, v);
            }
        }
        merged_extension_members
    }
}

impl Display for ApiError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_fmt(format_args!("{:?}", self))
    }
}

impl Reply for ApiError {
    fn into_response(self) -> Response {
        let res = json(&self);
        let mut res = res.into_response();
        *res.status_mut() = self.status;
        res.headers_mut().insert(
            "Content-Type",
            HeaderValue::from_static("application/problem+json"),
        );
        res
    }
}

impl StdError for ApiError {}

impl From<ApiError> for Rejection {
    fn from(from: ApiError) -> Self {
        custom(from)
    }
}

lazy_static! {
    static ref MISSING_FIELD_REGEX: Regex = Regex::new("missing field `(.*)`").unwrap();
}

#[derive(Clone, Debug)]
pub struct JsonDeserializeError {
    pub category: Category,
    pub detail: String,
    pub path: serde_path_to_error::Path,
}

impl StdError for JsonDeserializeError {}

impl Display for JsonDeserializeError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_fmt(format_args!("{:?}", self))
    }
}

impl Reply for JsonDeserializeError {
    fn into_response(self) -> Response {
        let mut extension_members = Map::new();

        // invalid-params should be a plural form even if it is always an array with a single value
        // for the future extendability.

        // if `path` has segments and the first value is not Unknown
        if let Some(segment) = self.path.iter().next() {
            match segment {
                serde_path_to_error::Segment::Unknown => {}
                _ => {
                    let invalid_params = serde_json::json!([ { "name": self.path.to_string() } ]);
                    extension_members.insert("invalid-params".to_string(), invalid_params);
                }
            }
        }

        // if detail contains missing field error
        // it seems that there is no way to handle this cleanly
        if let Some(captures) = MISSING_FIELD_REGEX.captures(&self.detail) {
            if let Some(r#match) = captures.get(1) {
                let invalid_params =
                    serde_json::json!([ { "name": r#match.as_str(), "type": "missing" } ]);
                extension_members.insert("invalid-params".to_string(), invalid_params);
            }
        }

        let api_error_type = match self.category {
            Category::Syntax => &JSON_SYNTAX_TYPE,
            Category::Data => &JSON_DATA_TYPE,
            Category::Eof => &JSON_EOF_TYPE,
            Category::Io => &JSON_IO_TYPE,
        };
        let detail = self.detail;
        let extension_members = match extension_members.keys().len() {
            0 => None,
            _ => Some(extension_members),
        };

        ApiError::from_api_error_type(api_error_type)
            .detail(detail)
            .extension_members(extension_members)
            .into_response()
    }
}

impl From<JsonDeserializeError> for Rejection {
    fn from(from: JsonDeserializeError) -> Self {
        custom(from)
    }
}

// Receives `ApiError`s and `JsonDeserializeError` and return it in the RFC7807 format.
pub fn default_rejection_handler(err: warp::Rejection) -> Result<Response, Rejection> {
    if let Some(api_error) = err.find_cause::<ApiError>() {
        Ok(api_error.clone().into_response())
    } else if let Some(json_error) = err.find_cause::<JsonDeserializeError>() {
        Ok(json_error.clone().into_response())
    } else if err.status() == http::status::StatusCode::METHOD_NOT_ALLOWED {
        Ok(ApiError::from_api_error_type(&DEFAULT_METHOD_NOT_ALLOWED_TYPE).into_response())
    } else {
        Err(err)
    }
}