ts_webapi/
error_response.rs

1//! A standardised error response from the API.
2
3use core::panic::Location;
4
5use bytes::Bytes;
6use http::{HeaderValue, Response, StatusCode, header};
7use http_body::Body;
8use serde::{Deserialize, Serialize};
9
10use crate::middleware::response_body::ResponseBody;
11
12#[derive(Clone, Debug, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14/// A problem detailing part of the error response.
15pub struct Problem {
16    /// A JSON path that identifies the part of the request that was the cause of the problem.
17    pub pointer: String,
18    /// A human-readable explanation specific to this occurrence of the problem.
19    pub detail: String,
20}
21impl Problem {
22    /// Create a new problem from a pointer and some details.
23    pub fn new<S1: ToString, S2: ToString>(pointer: S1, detail: S2) -> Self {
24        Self {
25            pointer: pointer.to_string(),
26            detail: detail.to_string(),
27        }
28    }
29}
30
31/// JSON payload for an error response.
32#[derive(Clone, Debug, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct ErrorResponse {
35    #[serde(skip)]
36    /// Status code of the response
37    pub status: StatusCode,
38    /// The list of problems to relay to the caller.
39    #[serde(skip_serializing_if = "Vec::is_empty")]
40    pub problems: Vec<Problem>,
41}
42
43impl ErrorResponse {
44    /// Convenience function for an internal server error response.
45    #[track_caller]
46    pub fn internal_server_error() -> Self {
47        log::error!("[{}] internal server error", Location::caller());
48        Self {
49            status: StatusCode::INTERNAL_SERVER_ERROR,
50            problems: vec![],
51        }
52    }
53
54    /// Convenience function for an unauthenticated response.
55    #[track_caller]
56    pub fn unauthenticated() -> Self {
57        log::warn!("[{}] request was unauthenticated", Location::caller());
58        Self {
59            status: StatusCode::UNAUTHORIZED,
60            problems: vec![],
61        }
62    }
63
64    /// Convenience function for a bad request response, with a single problem to present to the
65    /// caller.
66    pub fn basic_bad_request<S1: ToString, S2: ToString>(pointer: S1, detail: S2) -> Self {
67        Self::bad_request(vec![Problem::new(pointer, detail)])
68    }
69
70    /// Convenience function for a bad request response, with a set of problems to present to the
71    /// caller.
72    pub fn bad_request(problems: Vec<Problem>) -> Self {
73        Self {
74            status: StatusCode::BAD_REQUEST,
75            problems,
76        }
77    }
78
79    /// Convenience function for when part of the request was not able to be processed.
80    #[track_caller]
81    pub fn unprocessable_entity() -> Self {
82        log::warn!("[{}] request was unprocessable", Location::caller());
83        Self {
84            status: StatusCode::UNPROCESSABLE_ENTITY,
85            problems: vec![],
86        }
87    }
88
89    /// Convenience function for a not found response.
90    pub fn not_found() -> Self {
91        Self {
92            status: StatusCode::NOT_FOUND,
93            problems: vec![],
94        }
95    }
96
97    /// Converts the error response to an HTTP response.
98    ///
99    /// ## Panics
100    /// * If any of the responses cannot be built.
101    pub fn as_response<B>(self) -> Response<ResponseBody<B>>
102    where
103        B: Body,
104    {
105        let response = Response::builder().status(self.status);
106
107        if self.problems.is_empty() {
108            response
109                .body(ResponseBody::empty())
110                .expect("error response should always produce a valid response")
111        } else {
112            let json =
113                serde_json::to_string(&self).expect("error response should always be serializable");
114            let bytes = Bytes::from(json);
115            response
116                .header(
117                    header::CONTENT_TYPE,
118                    HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
119                )
120                .body(ResponseBody::full(bytes))
121                .expect("error response should always produce a valid response")
122        }
123    }
124}
125
126#[cfg(feature = "axum")]
127impl axum::response::IntoResponse for ErrorResponse {
128    fn into_response(self) -> axum::response::Response {
129        self.as_response::<axum::body::Body>().into_response()
130    }
131}
132
133#[cfg(feature = "axum")]
134impl From<axum::extract::rejection::JsonRejection> for ErrorResponse {
135    #[track_caller]
136    fn from(value: axum::extract::rejection::JsonRejection) -> Self {
137        log::warn!(
138            "request contained an unprocessable body ({}): {}",
139            value.status(),
140            value.body_text()
141        );
142        Self::unprocessable_entity()
143    }
144}