cat_gateway/service/common/responses/
mod.rs

1//! Generic Responses are all contained in their own modules, grouped by response codes.
2
3use 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::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};
36
37/// Default error responses
38#[derive(ApiResponse)]
39pub(crate) enum ErrorResponses {
40    /// ## Bad Request
41    ///
42    /// The client has not sent valid request, could be an invalid HTTP in general or
43    /// provided not correct headers, path or query arguments.
44    #[oai(status = 400)]
45    #[allow(dead_code)]
46    BadRequest,
47
48    /// ## Unauthorized
49    ///
50    /// The client has not sent valid authentication credentials for the requested
51    /// resource.
52    #[oai(status = 401)]
53    Unauthorized(Json<Unauthorized>),
54
55    /// ## Forbidden
56    ///
57    /// The client has not sent valid authentication credentials for the requested
58    /// resource.
59    #[oai(status = 403)]
60    Forbidden(Json<Forbidden>),
61
62    /// ## URI Too Long
63    ///
64    /// The client sent a request with the URI is longer than the server is willing to
65    /// interpret
66    #[oai(status = 414)]
67    #[allow(dead_code)]
68    UriTooLong,
69
70    /// ## Precondition Failed
71    ///
72    /// The client has not sent valid data in its request, headers, parameters or body.
73    #[oai(status = 412)]
74    PreconditionFailed(Json<PreconditionFailed>),
75
76    /// ## Too Many Requests
77    ///
78    /// The client has sent too many requests in a given amount of time.
79    #[oai(status = 429)]
80    TooManyRequests(
81        Json<TooManyRequests>,
82        #[oai(header = "Retry-After")] RetryAfterHeader,
83    ),
84
85    /// ## Request Header Fields Too Large
86    ///
87    /// The client sent a request with too large header fields.
88    #[oai(status = 431)]
89    #[allow(dead_code)]
90    RequestHeaderFieldsTooLarge,
91
92    /// ## Internal Server Error.
93    ///
94    /// An internal server error occurred.
95    ///
96    /// *The contents of this response should be reported to the projects issue tracker.*
97    #[oai(status = 500)]
98    ServerError(Json<InternalServerError>),
99
100    /// ## Service Unavailable
101    ///
102    /// The service is not available, try again later.
103    ///
104    /// *This is returned when the service either has not started,
105    /// or has become unavailable.*
106    #[oai(status = 503)]
107    ServiceUnavailable(
108        Json<ServiceUnavailable>,
109        #[oai(header = "Retry-After")] Option<RetryAfterHeader>,
110    ),
111}
112
113impl ErrorResponses {
114    /// Handle a 401 unauthorized response.
115    ///
116    /// Returns a 401 Unauthorized response.
117    /// Its OK if we actually never call this.  Required for the API.
118    /// May be generated by the ingress.
119    pub(crate) fn unauthorized() -> Self {
120        let error = Unauthorized::new(None);
121        ErrorResponses::Unauthorized(Json(error))
122    }
123
124    /// Handle a 403 forbidden response.
125    ///
126    /// Returns a 403 Forbidden response.
127    /// Its OK if we actually never call this.  Required for the API.
128    /// May be generated by the ingress.
129    pub(crate) fn forbidden(roles: Option<Vec<String>>) -> Self {
130        let error = Forbidden::new(None, roles);
131        ErrorResponses::Forbidden(Json(error))
132    }
133}
134
135/// Combine provided responses type with the default responses under one type.
136pub(crate) enum WithErrorResponses<T> {
137    /// Provided responses
138    With(T),
139    /// Error responses
140    Error(ErrorResponses),
141}
142
143impl<T> WithErrorResponses<T> {
144    /// Handle a 5xx response.
145    /// Returns a Server Error or a Service Unavailable response.
146    pub(crate) fn handle_error(err: &anyhow::Error) -> Self {
147        match err {
148            err if err.is::<bb8::RunError<tokio_postgres::Error>>() => {
149                Self::service_unavailable(err, RetryAfterOption::Default)
150            },
151            err => Self::internal_error(err),
152        }
153    }
154
155    /// Handle a 503 service unavailable error response.
156    ///
157    /// Returns a 503 Service unavailable Error response.
158    pub(crate) fn service_unavailable(err: &anyhow::Error, retry: RetryAfterOption) -> Self {
159        let error = ServiceUnavailable::new(None);
160        error!(id=%error.id(), error=?err, retry_after=?retry);
161        let retry = match retry {
162            RetryAfterOption::Default => Some(RetryAfterHeader::default()),
163            RetryAfterOption::None => None,
164            RetryAfterOption::Some(value) => Some(value),
165        };
166        WithErrorResponses::Error(ErrorResponses::ServiceUnavailable(Json(error), retry))
167    }
168
169    /// Handle a 500 internal error response.
170    ///
171    /// Returns a 500 Internal Error response.
172    pub(crate) fn internal_error(err: &anyhow::Error) -> Self {
173        let error = InternalServerError::new(None);
174        error!(id=%error.id(), error=?err);
175        WithErrorResponses::Error(ErrorResponses::ServerError(Json(error)))
176    }
177
178    /// Handle a 401 unauthorized response.
179    ///
180    /// Returns a 401 Unauthorized response.
181    /// Its OK if we actually never call this.  Required for the API.
182    /// May be generated by the ingress.
183    pub(crate) fn unauthorized() -> Self {
184        WithErrorResponses::Error(ErrorResponses::unauthorized())
185    }
186
187    /// Handle a 403 forbidden response.
188    ///
189    /// Returns a 403 Forbidden response.
190    /// Its OK if we actually never call this.  Required for the API.
191    /// May be generated by the ingress.
192    #[allow(dead_code)]
193    pub(crate) fn forbidden(roles: Option<Vec<String>>) -> Self {
194        WithErrorResponses::Error(ErrorResponses::forbidden(roles))
195    }
196
197    /// Handle a 412 precondition failed response.
198    ///
199    /// Returns a 412 precondition failed response.
200    fn precondition_failed(errors: Vec<poem::Error>) -> Self {
201        let error = PreconditionFailed::new(errors);
202        WithErrorResponses::Error(ErrorResponses::PreconditionFailed(Json(error)))
203    }
204
205    /// Handle a 429 rate limiting response.
206    ///
207    /// Returns a 429 Rate limit response.
208    /// Its OK if we actually never call this.  Required for the API.
209    /// May be generated by the ingress.
210    #[allow(dead_code)]
211    pub(crate) fn rate_limit(retry_after: Option<RetryAfterHeader>) -> Self {
212        let retry_after = retry_after.unwrap_or_default();
213        let error = TooManyRequests::new(None);
214        WithErrorResponses::Error(ErrorResponses::TooManyRequests(Json(error), retry_after))
215    }
216}
217
218impl<T: ApiResponse> From<T> for WithErrorResponses<T> {
219    fn from(val: T) -> Self {
220        Self::With(val)
221    }
222}
223
224impl<T: ApiResponse> ApiResponse for WithErrorResponses<T> {
225    const BAD_REQUEST_HANDLER: bool = true;
226
227    fn meta() -> MetaResponses {
228        let t_meta = T::meta();
229        let default_meta = ErrorResponses::meta();
230
231        let mut responses = HashSet::new();
232        responses.extend(
233            t_meta
234                .responses
235                .into_iter()
236                .map(FilteredByStatusCodeResponse),
237        );
238        responses.extend(
239            default_meta
240                .responses
241                .into_iter()
242                .map(FilteredByStatusCodeResponse),
243        );
244
245        let responses =
246            responses
247                .into_iter()
248                .map(|val| {
249                    let mut response = val.0;
250                    // Make modifications to the responses to set common headers
251                    if let Some(status) = response.status {
252                        // Only 2xx and 4xx responses get RateLimit Headers.
253                        if (200..300).contains(&status) || (400..500).contains(&status) {
254                            response.headers.insert(0usize, MetaHeader {
255                                name: "RateLimit".to_string(),
256                                description: Some("RateLimit Header.".to_string()),
257                                required: false,
258                                deprecated: false,
259                                schema: <RateLimitHeader as poem_openapi::types::Type>::schema_ref(),
260                            });
261                        }
262
263                        // All responses get Access-Control-Allow-Origin headers
264                        response.headers.insert(0usize, MetaHeader {
265                            name: "Access-Control-Allow-Origin".to_string(),
266                            description: Some("Access-Control-Allow-Origin Header.".to_string()),
267                            required: false,
268                            deprecated: false,
269                            schema: <AccessControlAllowOriginHeader as poem_openapi::types::Type>::schema_ref(),
270                        });
271                    }
272                    response
273                })
274                .collect();
275
276        // Add Rate limiting headers to ALL 2xx and 4xx responses
277        // for response in responses.iter_mut() {
278        //    response.
279        //    debug!(response = response);
280        //}
281
282        MetaResponses { responses }
283    }
284
285    fn register(registry: &mut Registry) {
286        ErrorResponses::register(registry);
287        T::register(registry);
288    }
289
290    fn from_parse_request_error(err: poem::Error) -> Self {
291        if err.status() == StatusCode::UNAUTHORIZED {
292            WithErrorResponses::unauthorized()
293        } else {
294            WithErrorResponses::precondition_failed(vec![err])
295        }
296    }
297}
298
299impl<T: IntoResponse + Send> IntoResponse for WithErrorResponses<T> {
300    fn into_response(self) -> poem::Response {
301        match self {
302            Self::With(t) => t.into_response(),
303            Self::Error(default) => default.into_response(),
304        }
305    }
306}
307
308/// `FilteredByStatusCodeResponse` is used to filter out duplicate responses by status
309/// code.
310struct FilteredByStatusCodeResponse(MetaResponse);
311impl PartialEq for FilteredByStatusCodeResponse {
312    fn eq(&self, other: &Self) -> bool {
313        self.0.status.eq(&other.0.status)
314    }
315}
316impl Eq for FilteredByStatusCodeResponse {}
317impl Hash for FilteredByStatusCodeResponse {
318    fn hash<H: Hasher>(&self, state: &mut H) {
319        self.0.status.hash(state);
320    }
321}