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