cat_gateway/service/common/responses/
mod.rs1use std::{
4 collections::HashSet,
5 hash::{Hash, Hasher},
6};
7
8use code_401_unauthorized::Unauthorized;
9use code_403_forbidden::Forbidden;
10use code_412_precondition_failed::PreconditionFailed;
11use code_429_too_many_requests::TooManyRequests;
12use code_503_service_unavailable::ServiceUnavailable;
13use poem::{http::StatusCode, IntoResponse};
14use poem_openapi::{
15 payload::Json,
16 registry::{MetaHeader, MetaResponse, MetaResponses, Registry},
17 ApiResponse,
18};
19use tracing::{debug, error};
20
21mod code_401_unauthorized;
22mod code_403_forbidden;
23mod code_412_precondition_failed;
24mod code_429_too_many_requests;
25
26pub(crate) mod code_500_internal_server_error;
27pub(crate) mod code_503_service_unavailable;
28
29use code_500_internal_server_error::InternalServerError;
30
31use super::types::headers::{
32 access_control_allow_origin::AccessControlAllowOriginHeader,
33 ratelimit::RateLimitHeader,
34 retry_after::{RetryAfterHeader, RetryAfterOption},
35};
36use crate::{
37 db::{event::EventDBConnectionError, index::session::CassandraSessionError},
38 service::utilities::health::{set_event_db_liveness, set_index_db_liveness},
39};
40
41#[derive(ApiResponse)]
43pub(crate) enum ErrorResponses {
44 #[oai(status = 400)]
49 #[allow(dead_code)]
50 BadRequest,
51
52 #[oai(status = 401)]
57 Unauthorized(Json<Unauthorized>),
58
59 #[oai(status = 403)]
64 Forbidden(Json<Forbidden>),
65
66 #[oai(status = 414)]
71 #[allow(dead_code)]
72 UriTooLong,
73
74 #[oai(status = 412)]
78 PreconditionFailed(Json<PreconditionFailed>),
79
80 #[oai(status = 429)]
84 TooManyRequests(
85 Json<TooManyRequests>,
86 #[oai(header = "Retry-After")] RetryAfterHeader,
87 ),
88
89 #[oai(status = 431)]
93 #[allow(dead_code)]
94 RequestHeaderFieldsTooLarge,
95
96 #[oai(status = 500)]
102 ServerError(Json<InternalServerError>),
103
104 #[oai(status = 503)]
111 ServiceUnavailable(
112 Json<ServiceUnavailable>,
113 #[oai(header = "Retry-After")] Option<RetryAfterHeader>,
114 ),
115}
116
117impl ErrorResponses {
118 pub(crate) fn unauthorized(text: String) -> Self {
124 let error = Unauthorized::new(Some(text));
125 ErrorResponses::Unauthorized(Json(error))
126 }
127
128 pub(crate) fn forbidden(roles: Option<Vec<String>>) -> Self {
134 let error = Forbidden::new(None, roles);
135 ErrorResponses::Forbidden(Json(error))
136 }
137}
138
139pub(crate) enum WithErrorResponses<T> {
141 With(T),
143 Error(ErrorResponses),
145}
146
147impl<T> WithErrorResponses<T> {
148 pub(crate) fn handle_error(err: &anyhow::Error) -> Self {
151 match err {
152 err if err.is::<EventDBConnectionError>() => {
153 debug!(error=?err, "Handling Response for Event DB Error");
155 set_event_db_liveness(false);
156 Self::service_unavailable(err, RetryAfterOption::Default)
157 },
158 err if err.is::<CassandraSessionError>() => {
159 debug!(error=?err, "Handling Response for Index DB Error");
161 set_index_db_liveness(false);
162 Self::service_unavailable(err, RetryAfterOption::Default)
163 },
164 err => {
165 debug!(error=?err, "Handling Response for Internal Error");
166 Self::internal_error(err)
167 },
168 }
169 }
170
171 pub(crate) fn service_unavailable_with_msg(
177 msg: String,
178 retry: RetryAfterOption,
179 ) -> Self {
180 let error = ServiceUnavailable::new(Some(msg));
181 let retry = match retry {
182 RetryAfterOption::Default => Some(RetryAfterHeader::default()),
183 RetryAfterOption::None => None,
184 RetryAfterOption::Some(value) => Some(value),
185 };
186 WithErrorResponses::Error(ErrorResponses::ServiceUnavailable(Json(error), retry))
187 }
188
189 pub(crate) fn service_unavailable(
193 err: &anyhow::Error,
194 retry: RetryAfterOption,
195 ) -> Self {
196 let error = ServiceUnavailable::new(None);
197 error!(id=%error.id(), error=?err, retry_after=?retry);
198 let retry = match retry {
199 RetryAfterOption::Default => Some(RetryAfterHeader::default()),
200 RetryAfterOption::None => None,
201 RetryAfterOption::Some(value) => Some(value),
202 };
203 WithErrorResponses::Error(ErrorResponses::ServiceUnavailable(Json(error), retry))
204 }
205
206 pub(crate) fn internal_error(err: &anyhow::Error) -> Self {
210 let error = InternalServerError::new(None);
211 error!(id=%error.id(), error=?err);
212 WithErrorResponses::Error(ErrorResponses::ServerError(Json(error)))
213 }
214
215 pub(crate) fn unauthorized(text: String) -> Self {
221 WithErrorResponses::Error(ErrorResponses::unauthorized(text))
222 }
223
224 #[allow(dead_code)]
230 pub(crate) fn forbidden(roles: Option<Vec<String>>) -> Self {
231 WithErrorResponses::Error(ErrorResponses::forbidden(roles))
232 }
233
234 fn precondition_failed(errors: Vec<poem::Error>) -> Self {
238 let error = PreconditionFailed::new(errors);
239 WithErrorResponses::Error(ErrorResponses::PreconditionFailed(Json(error)))
240 }
241
242 #[allow(dead_code)]
248 pub(crate) fn rate_limit(retry_after: Option<RetryAfterHeader>) -> Self {
249 let retry_after = retry_after.unwrap_or_default();
250 let error = TooManyRequests::new(None);
251 WithErrorResponses::Error(ErrorResponses::TooManyRequests(Json(error), retry_after))
252 }
253}
254
255impl<T: ApiResponse> From<T> for WithErrorResponses<T> {
256 fn from(val: T) -> Self {
257 Self::With(val)
258 }
259}
260
261impl<T: ApiResponse> ApiResponse for WithErrorResponses<T> {
262 const BAD_REQUEST_HANDLER: bool = true;
263
264 fn meta() -> MetaResponses {
265 let t_meta = T::meta();
266 let default_meta = ErrorResponses::meta();
267
268 let mut responses = HashSet::new();
269 responses.extend(
270 t_meta
271 .responses
272 .into_iter()
273 .map(FilteredByStatusCodeResponse),
274 );
275 responses.extend(
276 default_meta
277 .responses
278 .into_iter()
279 .map(FilteredByStatusCodeResponse),
280 );
281
282 let responses =
283 responses
284 .into_iter()
285 .map(|val| {
286 let mut response = val.0;
287 if let Some(status) = response.status {
289 if (200..300).contains(&status) || (400..500).contains(&status) {
291 response.headers.insert(0usize, MetaHeader {
292 name: "RateLimit".to_string(),
293 description: Some("RateLimit Header.".to_string()),
294 required: false,
295 deprecated: false,
296 schema: <RateLimitHeader as poem_openapi::types::Type>::schema_ref(),
297 });
298 }
299
300 response.headers.insert(0usize, MetaHeader {
302 name: "Access-Control-Allow-Origin".to_string(),
303 description: Some("Access-Control-Allow-Origin Header.".to_string()),
304 required: false,
305 deprecated: false,
306 schema: <AccessControlAllowOriginHeader as poem_openapi::types::Type>::schema_ref(),
307 });
308 }
309 response
310 })
311 .collect();
312
313 MetaResponses { responses }
320 }
321
322 fn register(registry: &mut Registry) {
323 ErrorResponses::register(registry);
324 T::register(registry);
325 }
326
327 fn from_parse_request_error(err: poem::Error) -> Self {
328 if err.status() == StatusCode::UNAUTHORIZED {
329 WithErrorResponses::unauthorized(err.to_string())
330 } else if err.status() == StatusCode::FORBIDDEN {
331 WithErrorResponses::forbidden(Some(vec![err.to_string()]))
332 } else {
333 WithErrorResponses::precondition_failed(vec![err])
334 }
335 }
336}
337
338impl<T: IntoResponse + Send> IntoResponse for WithErrorResponses<T> {
339 fn into_response(self) -> poem::Response {
340 match self {
341 Self::With(t) => t.into_response(),
342 Self::Error(default) => default.into_response(),
343 }
344 }
345}
346
347struct FilteredByStatusCodeResponse(MetaResponse);
350impl PartialEq for FilteredByStatusCodeResponse {
351 fn eq(
352 &self,
353 other: &Self,
354 ) -> bool {
355 self.0.status.eq(&other.0.status)
356 }
357}
358impl Eq for FilteredByStatusCodeResponse {}
359impl Hash for FilteredByStatusCodeResponse {
360 fn hash<H: Hasher>(
361 &self,
362 state: &mut H,
363 ) {
364 self.0.status.hash(state);
365 }
366}