ts_webapi/
error_response.rs1use 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")]
14pub struct Problem {
16 pub pointer: String,
18 pub detail: String,
20}
21impl Problem {
22 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#[derive(Clone, Debug, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct ErrorResponse {
35 #[serde(skip)]
36 pub status: StatusCode,
38 #[serde(skip_serializing_if = "Vec::is_empty")]
40 pub problems: Vec<Problem>,
41}
42
43impl ErrorResponse {
44 #[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 #[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 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 pub fn bad_request(problems: Vec<Problem>) -> Self {
73 Self {
74 status: StatusCode::BAD_REQUEST,
75 problems,
76 }
77 }
78
79 #[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 pub fn not_found() -> Self {
91 Self {
92 status: StatusCode::NOT_FOUND,
93 problems: vec![],
94 }
95 }
96
97 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}