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 pub fn internal_server_error() -> Self {
46 Self {
47 status: StatusCode::INTERNAL_SERVER_ERROR,
48 problems: vec![],
49 }
50 }
51
52 #[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 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 pub fn bad_request(problems: Vec<Problem>) -> Self {
71 Self {
72 status: StatusCode::BAD_REQUEST,
73 problems,
74 }
75 }
76
77 #[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 pub fn not_found() -> Self {
89 Self {
90 status: StatusCode::NOT_FOUND,
91 problems: vec![],
92 }
93 }
94
95 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}