1use axum::{
2 Json,
3 http::StatusCode,
4 response::{IntoResponse, Response},
5};
6use serde::Serialize;
7use thiserror::Error;
8
9type ErrorResponseTuple<'a> = (StatusCode, &'static str, &'a str);
12
13#[derive(Debug, Error)]
14pub enum AppError {
15 #[error("{0}")]
16 BadRequest(String),
17 #[error("{0}")]
18 Unauthorized(String),
19 #[error("{0}")]
20 Forbidden(String),
21 #[error("{0}")]
22 NotFound(String),
23 #[error("{0}")]
24 Conflict(String),
25 #[error("rate limited")]
26 TooManyRequests,
27 #[error("internal error")]
28 Internal(#[source] anyhow::Error),
29}
30
31#[derive(Serialize)]
32struct ErrorBody<'a> {
33 code: &'a str,
34 message: &'a str,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 request_id: Option<&'a str>,
37 #[cfg(feature = "debug-errors")]
38 #[serde(skip_serializing_if = "Option::is_none")]
39 details: Option<&'a str>,
40}
41
42impl IntoResponse for AppError {
43 fn into_response(self) -> Response {
44 use AppError::{
45 BadRequest, Conflict, Forbidden, Internal, NotFound, TooManyRequests, Unauthorized,
46 };
47
48 let request_id = "unknown";
51
52 #[cfg(feature = "debug-errors")]
54 let mut dbg_details: Option<String> = None;
55
56 let (status, code, safe_msg): ErrorResponseTuple = match &self {
58 BadRequest(m) => (StatusCode::BAD_REQUEST, "bad_request", m.as_str()),
59 Unauthorized(m) => (StatusCode::UNAUTHORIZED, "unauthorized", m.as_str()),
60 Forbidden(m) => (StatusCode::FORBIDDEN, "forbidden", m.as_str()),
61 NotFound(m) => (StatusCode::NOT_FOUND, "not_found", m.as_str()),
62 Conflict(m) => (StatusCode::CONFLICT, "conflict", m.as_str()),
63 TooManyRequests => (
64 StatusCode::TOO_MANY_REQUESTS,
65 "rate_limited",
66 "rate limited",
67 ),
68 #[cfg_attr(not(feature = "debug-errors"), allow(unused_variables))]
69 Internal(err) => {
70 #[cfg(feature = "debug-errors")]
71 {
72 dbg_details = Some(err.to_string());
74 }
75 (
76 StatusCode::INTERNAL_SERVER_ERROR,
77 "internal_error",
78 "internal error",
79 )
80 }
81 };
82
83 match &self {
85 Internal(err) => tracing::error!(
86 request_id = %request_id,
87 error = %err,
88 status = status.as_u16(),
89 "request failed"
90 ),
91 other => tracing::warn!(
92 request_id = %request_id,
93 error = %other,
94 status = status.as_u16(),
95 "request failed"
96 ),
97 }
98
99 let body = ErrorBody {
101 code,
102 message: safe_msg,
103 request_id: Some(request_id),
104 #[cfg(feature = "debug-errors")]
105 details: dbg_details.as_deref(),
106 };
107
108 (status, Json(body)).into_response()
109 }
110}