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