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    pub fn internal_server_error() -> Self {
46        Self {
47            status: StatusCode::INTERNAL_SERVER_ERROR,
48            problems: vec![],
49        }
50    }
51
52    /// Convenience function for an unauthenticated response.
53    #[track_caller]
54    pub fn unauthenticated() -> Self {
55        log::warn!("[{}] request was unauthenticated", Location::caller());
56        Self {
57            status: StatusCode::UNAUTHORIZED,
58            problems: vec![],
59        }
60    }
61
62    /// Convenience function for a bad request response, with a single problem to present to the
63    /// caller.
64    pub fn basic_bad_request<S1: ToString, S2: ToString>(pointer: S1, detail: S2) -> Self {
65        Self::bad_request(vec![Problem::new(pointer, detail)])
66    }
67
68    /// Convenience function for a bad request response, with a set of problems to present to the
69    /// caller.
70    pub fn bad_request(problems: Vec<Problem>) -> Self {
71        Self {
72            status: StatusCode::BAD_REQUEST,
73            problems,
74        }
75    }
76
77    /// Convenience function for when part of the request was not able to be processed.
78    #[track_caller]
79    pub fn unprocessable_entity() -> Self {
80        log::warn!("[{}] request was unprocessable", Location::caller());
81        Self {
82            status: StatusCode::UNPROCESSABLE_ENTITY,
83            problems: vec![],
84        }
85    }
86
87    /// Convenience function for a not found response.
88    pub fn not_found() -> Self {
89        Self {
90            status: StatusCode::NOT_FOUND,
91            problems: vec![],
92        }
93    }
94
95    /// Converts the error response to an HTTP response.
96    ///
97    /// ## Panics
98    /// * If any of the responses cannot be built.
99    pub fn as_response<B>(self) -> Response<ResponseBody<B>>
100    where
101        B: Body,
102    {
103        let response = Response::builder().status(self.status);
104
105        if self.problems.is_empty() {
106            response
107                .body(ResponseBody::empty())
108                .expect("error response should always produce a valid response")
109        } else {
110            let json =
111                serde_json::to_string(&self).expect("error response should always be serializable");
112            let bytes = Bytes::from(json);
113            response
114                .header(
115                    header::CONTENT_TYPE,
116                    HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
117                )
118                .body(ResponseBody::full(bytes))
119                .expect("error response should always produce a valid response")
120        }
121    }
122}
123
124#[cfg(feature = "axum")]
125impl axum::response::IntoResponse for ErrorResponse {
126    fn into_response(self) -> axum::response::Response {
127        self.as_response::<axum::body::Body>().into_response()
128    }
129}
130
131#[cfg(feature = "axum")]
132impl From<axum::extract::rejection::JsonRejection> for ErrorResponse {
133    fn from(value: axum::extract::rejection::JsonRejection) -> Self {
134        log::warn!(
135            "request contained an unprocessable body ({}): {}",
136            value.status(),
137            value.body_text()
138        );
139        Self::unprocessable_entity()
140    }
141}