ts_webapi/
error_response.rs1use 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")]
12pub struct Problem {
14 pub pointer: String,
16 pub detail: String,
18}
19impl Problem {
20 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#[derive(Clone, Debug, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct ErrorResponse {
33 #[serde(skip)]
34 pub status: StatusCode,
36 #[serde(skip_serializing_if = "Vec::is_empty")]
38 pub problems: Vec<Problem>,
39}
40
41impl ErrorResponse {
42 pub fn internal_server_error() -> Self {
44 Self {
45 status: StatusCode::INTERNAL_SERVER_ERROR,
46 problems: vec![],
47 }
48 }
49
50 #[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 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 pub fn bad_request(problems: Vec<Problem>) -> Self {
69 Self {
70 status: StatusCode::BAD_REQUEST,
71 problems,
72 }
73 }
74
75 #[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 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}