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::<bb8::RunError<tokio_postgres::Error>>()
153 || err.is::<EventDBConnectionError>() =>
154 {
155 debug!(error=?err, "Handling Response for Event DB Error");
157 set_event_db_liveness(false);
158 Self::service_unavailable(err, RetryAfterOption::Default)
159 },
160 err if err.is::<CassandraSessionError>() => {
161 debug!(error=?err, "Handling Response for Index DB Error");
163 set_index_db_liveness(false);
164 Self::service_unavailable(err, RetryAfterOption::Default)
165 },
166 err => {
167 debug!(error=?err, "Handling Response for Internal Error");
168 Self::internal_error(err)
169 },
170 }
171 }
172
173 pub(crate) fn service_unavailable_with_msg(msg: String, retry: RetryAfterOption) -> Self {
179 let error = ServiceUnavailable::new(Some(msg));
180 let retry = match retry {
181 RetryAfterOption::Default => Some(RetryAfterHeader::default()),
182 RetryAfterOption::None => None,
183 RetryAfterOption::Some(value) => Some(value),
184 };
185 WithErrorResponses::Error(ErrorResponses::ServiceUnavailable(Json(error), retry))
186 }
187
188 pub(crate) fn service_unavailable(err: &anyhow::Error, retry: RetryAfterOption) -> Self {
192 let error = ServiceUnavailable::new(None);
193 error!(id=%error.id(), error=?err, retry_after=?retry);
194 let retry = match retry {
195 RetryAfterOption::Default => Some(RetryAfterHeader::default()),
196 RetryAfterOption::None => None,
197 RetryAfterOption::Some(value) => Some(value),
198 };
199 WithErrorResponses::Error(ErrorResponses::ServiceUnavailable(Json(error), retry))
200 }
201
202 pub(crate) fn internal_error(err: &anyhow::Error) -> Self {
206 let error = InternalServerError::new(None);
207 error!(id=%error.id(), error=?err);
208 WithErrorResponses::Error(ErrorResponses::ServerError(Json(error)))
209 }
210
211 pub(crate) fn unauthorized(text: String) -> Self {
217 WithErrorResponses::Error(ErrorResponses::unauthorized(text))
218 }
219
220 #[allow(dead_code)]
226 pub(crate) fn forbidden(roles: Option<Vec<String>>) -> Self {
227 WithErrorResponses::Error(ErrorResponses::forbidden(roles))
228 }
229
230 fn precondition_failed(errors: Vec<poem::Error>) -> Self {
234 let error = PreconditionFailed::new(errors);
235 WithErrorResponses::Error(ErrorResponses::PreconditionFailed(Json(error)))
236 }
237
238 #[allow(dead_code)]
244 pub(crate) fn rate_limit(retry_after: Option<RetryAfterHeader>) -> Self {
245 let retry_after = retry_after.unwrap_or_default();
246 let error = TooManyRequests::new(None);
247 WithErrorResponses::Error(ErrorResponses::TooManyRequests(Json(error), retry_after))
248 }
249}
250
251impl<T: ApiResponse> From<T> for WithErrorResponses<T> {
252 fn from(val: T) -> Self {
253 Self::With(val)
254 }
255}
256
257impl<T: ApiResponse> ApiResponse for WithErrorResponses<T> {
258 const BAD_REQUEST_HANDLER: bool = true;
259
260 fn meta() -> MetaResponses {
261 let t_meta = T::meta();
262 let default_meta = ErrorResponses::meta();
263
264 let mut responses = HashSet::new();
265 responses.extend(
266 t_meta
267 .responses
268 .into_iter()
269 .map(FilteredByStatusCodeResponse),
270 );
271 responses.extend(
272 default_meta
273 .responses
274 .into_iter()
275 .map(FilteredByStatusCodeResponse),
276 );
277
278 let responses =
279 responses
280 .into_iter()
281 .map(|val| {
282 let mut response = val.0;
283 if let Some(status) = response.status {
285 if (200..300).contains(&status) || (400..500).contains(&status) {
287 response.headers.insert(0usize, MetaHeader {
288 name: "RateLimit".to_string(),
289 description: Some("RateLimit Header.".to_string()),
290 required: false,
291 deprecated: false,
292 schema: <RateLimitHeader as poem_openapi::types::Type>::schema_ref(),
293 });
294 }
295
296 response.headers.insert(0usize, MetaHeader {
298 name: "Access-Control-Allow-Origin".to_string(),
299 description: Some("Access-Control-Allow-Origin Header.".to_string()),
300 required: false,
301 deprecated: false,
302 schema: <AccessControlAllowOriginHeader as poem_openapi::types::Type>::schema_ref(),
303 });
304 }
305 response
306 })
307 .collect();
308
309 MetaResponses { responses }
316 }
317
318 fn register(registry: &mut Registry) {
319 ErrorResponses::register(registry);
320 T::register(registry);
321 }
322
323 fn from_parse_request_error(err: poem::Error) -> Self {
324 if err.status() == StatusCode::UNAUTHORIZED {
325 WithErrorResponses::unauthorized(err.to_string())
326 } else if err.status() == StatusCode::FORBIDDEN {
327 WithErrorResponses::forbidden(Some(vec![err.to_string()]))
328 } else {
329 WithErrorResponses::precondition_failed(vec![err])
330 }
331 }
332}
333
334impl<T: IntoResponse + Send> IntoResponse for WithErrorResponses<T> {
335 fn into_response(self) -> poem::Response {
336 match self {
337 Self::With(t) => t.into_response(),
338 Self::Error(default) => default.into_response(),
339 }
340 }
341}
342
343struct FilteredByStatusCodeResponse(MetaResponse);
346impl PartialEq for FilteredByStatusCodeResponse {
347 fn eq(&self, other: &Self) -> bool {
348 self.0.status.eq(&other.0.status)
349 }
350}
351impl Eq for FilteredByStatusCodeResponse {}
352impl Hash for FilteredByStatusCodeResponse {
353 fn hash<H: Hasher>(&self, state: &mut H) {
354 self.0.status.hash(state);
355 }
356}