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 416 : #[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("Shutting down")]
32 : ShuttingDown,
33 :
34 : #[error("Timeout")]
35 : Timeout(Cow<'static, str>),
36 :
37 : #[error(transparent)]
38 : InternalServerError(anyhow::Error),
39 : }
40 :
41 : impl ApiError {
42 297 : pub fn into_response(self) -> Response<Body> {
43 297 : match self {
44 35 : ApiError::BadRequest(err) => HttpErrorBody::response_from_msg_and_status(
45 35 : format!("{err:#?}"), // use debug printing so that we give the cause
46 35 : StatusCode::BAD_REQUEST,
47 35 : ),
48 : ApiError::Forbidden(_) => {
49 9 : HttpErrorBody::response_from_msg_and_status(self.to_string(), StatusCode::FORBIDDEN)
50 : }
51 5 : ApiError::Unauthorized(_) => HttpErrorBody::response_from_msg_and_status(
52 5 : self.to_string(),
53 5 : StatusCode::UNAUTHORIZED,
54 5 : ),
55 : ApiError::NotFound(_) => {
56 145 : HttpErrorBody::response_from_msg_and_status(self.to_string(), StatusCode::NOT_FOUND)
57 : }
58 : ApiError::Conflict(_) => {
59 23 : HttpErrorBody::response_from_msg_and_status(self.to_string(), StatusCode::CONFLICT)
60 : }
61 4 : ApiError::PreconditionFailed(_) => HttpErrorBody::response_from_msg_and_status(
62 4 : self.to_string(),
63 4 : StatusCode::PRECONDITION_FAILED,
64 4 : ),
65 29 : ApiError::ShuttingDown => HttpErrorBody::response_from_msg_and_status(
66 29 : "Shutting down".to_string(),
67 29 : StatusCode::SERVICE_UNAVAILABLE,
68 29 : ),
69 9 : ApiError::ResourceUnavailable(err) => HttpErrorBody::response_from_msg_and_status(
70 9 : err.to_string(),
71 9 : StatusCode::SERVICE_UNAVAILABLE,
72 9 : ),
73 0 : ApiError::Timeout(err) => HttpErrorBody::response_from_msg_and_status(
74 0 : err.to_string(),
75 0 : StatusCode::REQUEST_TIMEOUT,
76 0 : ),
77 38 : ApiError::InternalServerError(err) => HttpErrorBody::response_from_msg_and_status(
78 38 : err.to_string(),
79 38 : StatusCode::INTERNAL_SERVER_ERROR,
80 38 : ),
81 : }
82 297 : }
83 : }
84 :
85 314 : #[derive(Serialize, Deserialize)]
86 : pub struct HttpErrorBody {
87 : pub msg: String,
88 : }
89 :
90 : impl HttpErrorBody {
91 17 : pub fn from_msg(msg: String) -> Self {
92 17 : HttpErrorBody { msg }
93 17 : }
94 :
95 297 : pub fn response_from_msg_and_status(msg: String, status: StatusCode) -> Response<Body> {
96 297 : HttpErrorBody { msg }.to_response(status)
97 297 : }
98 :
99 297 : pub fn to_response(&self, status: StatusCode) -> Response<Body> {
100 297 : Response::builder()
101 297 : .status(status)
102 297 : .header(header::CONTENT_TYPE, "application/json")
103 297 : // we do not have nested maps with non string keys so serialization shouldn't fail
104 297 : .body(Body::from(serde_json::to_string(self).unwrap()))
105 297 : .unwrap()
106 297 : }
107 : }
108 :
109 8 : pub async fn route_error_handler(err: routerify::RouteError) -> Response<Body> {
110 8 : match err.downcast::<ApiError>() {
111 8 : Ok(api_error) => api_error_handler(*api_error),
112 0 : Err(other_error) => {
113 : // We expect all the request handlers to return an ApiError, so this should
114 : // not be reached. But just in case.
115 0 : error!("Error processing HTTP request: {other_error:?}");
116 0 : HttpErrorBody::response_from_msg_and_status(
117 0 : other_error.to_string(),
118 0 : StatusCode::INTERNAL_SERVER_ERROR,
119 0 : )
120 : }
121 : }
122 8 : }
123 :
124 297 : pub fn api_error_handler(api_error: ApiError) -> Response<Body> {
125 297 : // Print a stack trace for Internal Server errors
126 297 :
127 297 : match api_error {
128 : ApiError::Forbidden(_) | ApiError::Unauthorized(_) => {
129 14 : warn!("Error processing HTTP request: {api_error:#}")
130 : }
131 9 : ApiError::ResourceUnavailable(_) => info!("Error processing HTTP request: {api_error:#}"),
132 145 : ApiError::NotFound(_) => info!("Error processing HTTP request: {api_error:#}"),
133 38 : ApiError::InternalServerError(_) => error!("Error processing HTTP request: {api_error:?}"),
134 29 : ApiError::ShuttingDown => info!("Shut down while processing HTTP request"),
135 0 : ApiError::Timeout(_) => info!("Timeout while processing HTTP request: {api_error:#}"),
136 62 : _ => info!("Error processing HTTP request: {api_error:#}"),
137 : }
138 :
139 297 : api_error.into_response()
140 297 : }
|