Line data Source code
1 : use std::borrow::Cow;
2 : use std::error::Error as StdError;
3 :
4 : use hyper::{Body, Response, StatusCode, header};
5 : use serde::{Deserialize, Serialize};
6 : use thiserror::Error;
7 : use tracing::{error, info, warn};
8 : use utils::auth::AuthError;
9 :
10 : #[derive(Debug, Error)]
11 : pub enum ApiError {
12 : #[error("Bad request: {0:#?}")]
13 : BadRequest(anyhow::Error),
14 :
15 : #[error("Forbidden: {0}")]
16 : Forbidden(String),
17 :
18 : #[error("Unauthorized: {0}")]
19 : Unauthorized(String),
20 :
21 : #[error("NotFound: {0}")]
22 : NotFound(Box<dyn StdError + Send + Sync + 'static>),
23 :
24 : #[error("Conflict: {0}")]
25 : Conflict(String),
26 :
27 : #[error("Precondition failed: {0}")]
28 : PreconditionFailed(Box<str>),
29 :
30 : #[error("Resource temporarily unavailable: {0}")]
31 : ResourceUnavailable(Cow<'static, str>),
32 :
33 : #[error("Too many requests: {0}")]
34 : TooManyRequests(Cow<'static, str>),
35 :
36 : #[error("Shutting down")]
37 : ShuttingDown,
38 :
39 : #[error("Timeout")]
40 : Timeout(Cow<'static, str>),
41 :
42 : #[error("Request cancelled")]
43 : Cancelled,
44 :
45 : #[error(transparent)]
46 : InternalServerError(anyhow::Error),
47 : }
48 :
49 : impl ApiError {
50 0 : pub fn into_response(self) -> Response<Body> {
51 0 : match self {
52 0 : ApiError::BadRequest(err) => HttpErrorBody::response_from_msg_and_status(
53 0 : format!("{err:#?}"), // use debug printing so that we give the cause
54 0 : StatusCode::BAD_REQUEST,
55 0 : ),
56 : ApiError::Forbidden(_) => {
57 0 : HttpErrorBody::response_from_msg_and_status(self.to_string(), StatusCode::FORBIDDEN)
58 : }
59 0 : ApiError::Unauthorized(_) => HttpErrorBody::response_from_msg_and_status(
60 0 : self.to_string(),
61 0 : StatusCode::UNAUTHORIZED,
62 0 : ),
63 : ApiError::NotFound(_) => {
64 0 : HttpErrorBody::response_from_msg_and_status(self.to_string(), StatusCode::NOT_FOUND)
65 : }
66 : ApiError::Conflict(_) => {
67 0 : HttpErrorBody::response_from_msg_and_status(self.to_string(), StatusCode::CONFLICT)
68 : }
69 0 : ApiError::PreconditionFailed(_) => HttpErrorBody::response_from_msg_and_status(
70 0 : self.to_string(),
71 0 : StatusCode::PRECONDITION_FAILED,
72 0 : ),
73 0 : ApiError::ShuttingDown => HttpErrorBody::response_from_msg_and_status(
74 0 : "Shutting down".to_string(),
75 0 : StatusCode::SERVICE_UNAVAILABLE,
76 0 : ),
77 0 : ApiError::ResourceUnavailable(err) => HttpErrorBody::response_from_msg_and_status(
78 0 : err.to_string(),
79 0 : StatusCode::SERVICE_UNAVAILABLE,
80 0 : ),
81 0 : ApiError::TooManyRequests(err) => HttpErrorBody::response_from_msg_and_status(
82 0 : err.to_string(),
83 0 : StatusCode::TOO_MANY_REQUESTS,
84 0 : ),
85 0 : ApiError::Timeout(err) => HttpErrorBody::response_from_msg_and_status(
86 0 : err.to_string(),
87 0 : StatusCode::REQUEST_TIMEOUT,
88 0 : ),
89 0 : ApiError::Cancelled => HttpErrorBody::response_from_msg_and_status(
90 0 : self.to_string(),
91 0 : StatusCode::INTERNAL_SERVER_ERROR,
92 0 : ),
93 0 : ApiError::InternalServerError(err) => HttpErrorBody::response_from_msg_and_status(
94 0 : format!("{err:#}"), // use alternative formatting so that we give the cause without backtrace
95 0 : StatusCode::INTERNAL_SERVER_ERROR,
96 0 : ),
97 : }
98 0 : }
99 : }
100 :
101 : impl From<AuthError> for ApiError {
102 0 : fn from(_value: AuthError) -> Self {
103 0 : // Don't pass on the value of the AuthError as a precautionary measure.
104 0 : // Being intentionally vague in public error communication hurts debugability
105 0 : // but it is more secure.
106 0 : ApiError::Forbidden("JWT authentication error".to_string())
107 0 : }
108 : }
109 :
110 0 : #[derive(Serialize, Deserialize)]
111 : pub struct HttpErrorBody {
112 : pub msg: String,
113 : }
114 :
115 : impl HttpErrorBody {
116 0 : pub fn from_msg(msg: String) -> Self {
117 0 : HttpErrorBody { msg }
118 0 : }
119 :
120 0 : pub fn response_from_msg_and_status(msg: String, status: StatusCode) -> Response<Body> {
121 0 : HttpErrorBody { msg }.to_response(status)
122 0 : }
123 :
124 0 : pub fn to_response(&self, status: StatusCode) -> Response<Body> {
125 0 : Response::builder()
126 0 : .status(status)
127 0 : .header(header::CONTENT_TYPE, "application/json")
128 0 : // we do not have nested maps with non string keys so serialization shouldn't fail
129 0 : .body(Body::from(serde_json::to_string(self).unwrap()))
130 0 : .unwrap()
131 0 : }
132 : }
133 :
134 0 : pub async fn route_error_handler(err: routerify::RouteError) -> Response<Body> {
135 0 : match err.downcast::<ApiError>() {
136 0 : Ok(api_error) => api_error_handler(*api_error),
137 0 : Err(other_error) => {
138 0 : // We expect all the request handlers to return an ApiError, so this should
139 0 : // not be reached. But just in case.
140 0 : error!("Error processing HTTP request: {other_error:?}");
141 0 : HttpErrorBody::response_from_msg_and_status(
142 0 : other_error.to_string(),
143 0 : StatusCode::INTERNAL_SERVER_ERROR,
144 0 : )
145 : }
146 : }
147 0 : }
148 :
149 0 : pub fn api_error_handler(api_error: ApiError) -> Response<Body> {
150 0 : // Print a stack trace for Internal Server errors
151 0 :
152 0 : match api_error {
153 : ApiError::Forbidden(_) | ApiError::Unauthorized(_) => {
154 0 : warn!("Error processing HTTP request: {api_error:#}")
155 : }
156 0 : ApiError::ResourceUnavailable(_) => info!("Error processing HTTP request: {api_error:#}"),
157 0 : ApiError::NotFound(_) => info!("Error processing HTTP request: {api_error:#}"),
158 0 : ApiError::InternalServerError(_) => error!("Error processing HTTP request: {api_error:?}"),
159 0 : ApiError::ShuttingDown => info!("Shut down while processing HTTP request"),
160 0 : ApiError::Timeout(_) => info!("Timeout while processing HTTP request: {api_error:#}"),
161 0 : ApiError::Cancelled => info!("Request cancelled while processing HTTP request"),
162 0 : _ => info!("Error processing HTTP request: {api_error:#}"),
163 : }
164 :
165 0 : api_error.into_response()
166 0 : }
|