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
// Copyright 2020 Oxide Computer Company
/*!
 * Generic server error handling facilities
 *
 * Error handling in an API
 * ------------------------
 *
 * Our approach for managing errors within the API server balances several
 * goals:
 *
 * * Every incoming HTTP request should conclude with a response, which is
 *   either successful (200-level or 300-level status code) or a failure
 *   (400-level for client errors, 500-level for server errors).
 * * There are several different sources of errors within an API server:
 *     * The HTTP layer of the server may generate an error.  In this case, it
 *       may be just as easy to generate the appropriate HTTP response (with a
 *       400-level or 500-level status code) as it would be to generate an Error
 *       object of some kind.
 *     * An HTTP-agnostic layer of the API server code base may generate an
 *       error.  It would be nice (but not essential) if these layers did not
 *       need to know about HTTP-specific things like status codes, particularly
 *       since they may not map straightforwardly.  For example, a NotFound
 *       error from the model may not result in a 404 out the API -- it might
 *       just mean that something in the model layer needs to create an object
 *       before using it.
 *     * A library that's not part of the API server code base may generate an
 *       error.  This would include standard library interfaces returning
 *       `std::io::Error` and Hyper returning `hyper::Error`, for examples.
 * * We'd like to take advantage of Rust's built-in error handling control flow
 *   tools, like Results and the '?' operator.
 *
 * Dropshot itself is concerned only with HTTP errors.  We define `HttpError`,
 * which provides a status code, error code (via an Enum), external message (for
 * sending in the response), optional metadata, and an internal message (for the
 * log file or other instrumentation).  The HTTP layers of the request-handling
 * stack may use this struct directly.  **The set of possible error codes here
 * is part of a service's OpenAPI contract, as is the schema for any metadata.**
 * By the time an error bubbles up to the top of the request handling stack, it
 * must be an HttpError.
 *
 * For the HTTP-agnostic layers of an API server (i.e., consumers of Dropshot),
 * we recommend a separate enum to represent their errors in an HTTP-agnostic
 * way.  Consumers can provide a `From` implementation that converts these
 * errors into HttpErrors.
 */

use hyper::error::Error as HyperError;
use serde::Deserialize;
use serde::Serialize;

/**
 * `HttpError` represents an error generated as part of handling an API
 * request.  When these bubble up to the top of the request handling stack
 * (which is most of the time that they're generated), these are turned into an
 * HTTP response, which includes:
 *
 *   * a status code, which is likely either 400-level (indicating a client
 *     error, like bad input) or 500-level (indicating a server error).
 *   * a structured (JSON) body, which includes:
 *       * a string error code, which identifies the underlying error condition
 *         so that clients can potentially make programmatic decisions based on
 *         the error type
 *       * a string error message, which is the human-readable summary of the
 *         issue, intended to make sense for API users (i.e., not API server
 *         developers)
 *       * optionally: additional metadata describing the issue.  For a
 *         validation error, this could include information about which
 *         parameter was invalid and why.  This should conform to a schema
 *         associated with the error code.
 *
 * It's easy to go overboard with the error codes and metadata.  Generally, we
 * should avoid creating specific codes and metadata unless there's a good
 * reason for a client to care.
 *
 * Besides that, `HttpError`s also have an internal error message, which may
 * differ from the error message that gets reported to users.  For example, if
 * the request fails because an internal database is unreachable, the client may
 * just see "internal error", while the server log would include more details
 * like "failed to acquire connection to database at 10.1.2.3".
 */
#[derive(Debug)]
pub struct HttpError {
    /*
     * TODO-coverage add coverage in the test suite for error_code
     * TODO-robustness should error_code just be required?  It'll be confusing
     * to clients if it's missing sometimes.  Should this class be parametrized
     * by some enum type?
     * TODO-polish add cause chain for a complete log message?
     */
    /** HTTP status code for this error */
    pub status_code: http::StatusCode,
    /**
     * Optional string error code for this error.  Callers are advised to
     * use an enum to populate this field.
     */
    pub error_code: Option<String>,
    /** Error message to be sent to API client for this error */
    pub external_message: String,
    /** Error message recorded in the log for this error */
    pub internal_message: String,
}

/**
 * Body of an HTTP response for an `HttpError`.  This type can be used to
 * deserialize an HTTP response corresponding to an error in order to access the
 * error code, message, etc.
 */
#[derive(Debug, Deserialize, Serialize)]
pub struct HttpErrorResponseBody {
    pub request_id: String,
    pub error_code: Option<String>,
    pub message: String,
}

impl From<HyperError> for HttpError {
    fn from(error: HyperError) -> Self {
        /*
         * TODO-correctness dig deeper into the various cases to make sure this
         * is a valid way to represent it.
         */
        HttpError::for_bad_request(
            None,
            format!("error processing request: {}", error),
        )
    }
}

impl From<http::Error> for HttpError {
    fn from(error: http::Error) -> Self {
        /*
         * TODO-correctness dig deeper into the various cases to make sure this
         * is a valid way to represent it.
         */
        HttpError::for_bad_request(
            None,
            format!("error processing request: {}", error),
        )
    }
}

impl HttpError {
    /**
     * Generates an `HttpError` for any 400-level client error with a custom
     * `message` used for both the internal and external message.  The
     * expectation here is that for most 400-level errors, there's no need for a
     * separate internal message.
     */
    pub fn for_client_error(
        error_code: Option<String>,
        status_code: http::StatusCode,
        message: String,
    ) -> Self {
        assert!(status_code.is_client_error());
        HttpError {
            status_code,
            error_code,
            internal_message: message.clone(),
            external_message: message,
        }
    }

    /**
     * Generates an `HttpError` for a 500 "Internal Server Error" error with the
     * given `internal_message` for the internal message.
     */
    pub fn for_internal_error(internal_message: String) -> Self {
        let status_code = http::StatusCode::INTERNAL_SERVER_ERROR;
        HttpError {
            status_code,
            error_code: Some(String::from("Internal")),
            external_message: status_code
                .canonical_reason()
                .unwrap()
                .to_string(),
            internal_message,
        }
    }

    /**
     * Generates an `HttpError` for a 503 "Service Unavailable" error with the
     * given `internal_message` for the internal message.
     */
    pub fn for_unavail(
        error_code: Option<String>,
        internal_message: String,
    ) -> Self {
        let status_code = http::StatusCode::SERVICE_UNAVAILABLE;
        HttpError {
            status_code,
            error_code,
            external_message: status_code
                .canonical_reason()
                .unwrap()
                .to_string(),
            internal_message,
        }
    }

    /**
     * Generates a 400 "Bad Request" error with the given `message` used for
     * both the internal and external message.  This is a convenience wrapper
     * around [`HttpError::for_client_error`].
     */
    pub fn for_bad_request(
        error_code: Option<String>,
        message: String,
    ) -> Self {
        HttpError::for_client_error(
            error_code,
            http::StatusCode::BAD_REQUEST,
            message,
        )
    }

    /**
     * Generates an `HttpError` for the given HTTP `status_code` where the
     * internal and external messages for the error come from the standard label
     * for this status code (e.g., the message for status code 404 is "Not
     * Found").
     */
    pub fn for_status(
        error_code: Option<String>,
        status_code: http::StatusCode,
    ) -> Self {
        /* TODO-polish This should probably be our own message. */
        let message = status_code.canonical_reason().unwrap().to_string();
        HttpError::for_client_error(error_code, status_code, message)
    }

    /**
     * Generates an `HttpError` for a 404 "Not Found" error with a custom
     * internal message `internal_message`.  The external message will be "Not
     * Found" (i.e., the standard label for status code 404).
     */
    pub fn for_not_found(
        error_code: Option<String>,
        internal_message: String,
    ) -> Self {
        let status_code = http::StatusCode::NOT_FOUND;
        let external_message =
            status_code.canonical_reason().unwrap().to_string();
        HttpError {
            status_code,
            error_code,
            internal_message,
            external_message,
        }
    }

    /**
     * Generates an HTTP response for the given `HttpError`, using `request_id`
     * for the response's request id.
     */
    pub fn into_response(
        self,
        request_id: &str,
    ) -> hyper::Response<hyper::Body> {
        /*
         * TODO-hardening: consider handling the operational errors that the
         * Serde serialization fails or the response construction fails.  In
         * those cases, we should probably try to report this as a serious
         * problem (e.g., to the log) and send back a 500-level response.  (Of
         * course, that could fail in the same way, but it's less likely because
         * there's only one possible set of input and we can test it.  We'll
         * probably have to use unwrap() there and make sure we've tested that
         * code at least once!)
         */
        hyper::Response::builder()
            .status(self.status_code)
            .header(
                http::header::CONTENT_TYPE,
                super::http_util::CONTENT_TYPE_JSON,
            )
            .header(super::http_util::HEADER_REQUEST_ID, request_id)
            .body(
                serde_json::to_string_pretty(&HttpErrorResponseBody {
                    request_id: request_id.to_string(),
                    message: self.external_message,
                    error_code: self.error_code,
                })
                .unwrap()
                .into(),
            )
            .unwrap()
    }
}