1use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use axum::Json;
6use serde::Serialize;
7
8#[derive(Debug, Serialize)]
10pub struct ErrorBody {
11 pub code: &'static str,
13 pub message: String,
15}
16
17#[derive(Debug)]
23pub struct AppError(bezant::Error);
24
25impl AppError {
26 #[must_use]
29 pub fn inner(&self) -> &bezant::Error {
30 &self.0
31 }
32}
33
34impl From<bezant::Error> for AppError {
35 fn from(value: bezant::Error) -> Self {
36 Self(value)
37 }
38}
39
40impl From<anyhow::Error> for AppError {
41 fn from(value: anyhow::Error) -> Self {
42 Self(bezant::Error::from(value))
43 }
44}
45
46impl IntoResponse for AppError {
47 fn into_response(self) -> Response {
48 let (status, code) = map_status(&self.0);
49
50 if status.is_server_error() {
56 tracing::error!(
57 code,
58 status = %status,
59 error = %self.0,
60 "request failed (5xx)"
61 );
62 } else {
63 tracing::warn!(
64 code,
65 status = %status,
66 error = %self.0,
67 "request failed (4xx)"
68 );
69 }
70
71 let body = ErrorBody {
72 code,
73 message: self.0.to_string(),
74 };
75 (status, Json(body)).into_response()
76 }
77}
78
79fn map_status(err: &bezant::Error) -> (StatusCode, &'static str) {
85 use bezant::Error as E;
86 match err {
87 E::InvalidBaseUrl(_) => (StatusCode::BAD_REQUEST, "invalid_base_url"),
88 E::UrlNotABase { .. } => (StatusCode::BAD_REQUEST, "url_not_a_base"),
89 E::Http(e) => {
90 if e.is_timeout() {
94 (StatusCode::GATEWAY_TIMEOUT, "upstream_timeout")
95 } else if e.is_connect() {
96 (StatusCode::SERVICE_UNAVAILABLE, "upstream_unreachable")
97 } else {
98 (StatusCode::BAD_GATEWAY, "upstream_http_error")
99 }
100 }
101 E::UpstreamStatus { status, .. } => {
102 let s = StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_GATEWAY);
106 if s.is_server_error() || s.as_u16() == 429 {
107 (s, "upstream_status")
108 } else if s.is_client_error() {
109 (s, "upstream_client_error")
110 } else {
111 (StatusCode::BAD_GATEWAY, "upstream_status")
112 }
113 }
114 E::Unknown { .. } => (StatusCode::BAD_GATEWAY, "upstream_unknown_variant"),
115 E::Decode { .. } => (StatusCode::BAD_GATEWAY, "upstream_decode_error"),
116 E::Api(_) => (StatusCode::BAD_GATEWAY, "upstream_api_error"),
117 E::BadRequest(_) => (StatusCode::BAD_REQUEST, "bad_request"),
118 E::MissingQuery { .. } => (StatusCode::BAD_REQUEST, "missing_query_param"),
119 E::Header { .. } => (StatusCode::BAD_REQUEST, "invalid_header_value"),
120 E::SymbolNotFound { .. } => (StatusCode::NOT_FOUND, "symbol_not_found"),
121 E::BadConid { .. } => (StatusCode::BAD_GATEWAY, "upstream_bad_conid"),
122 E::WsHandshake { .. } => (StatusCode::BAD_GATEWAY, "ws_handshake_failed"),
123 E::WsTransport { .. } => (StatusCode::BAD_GATEWAY, "ws_transport_failed"),
124 E::WsProtocol(_) => (StatusCode::BAD_GATEWAY, "ws_protocol_error"),
125 E::ResponseBuild(_) => (StatusCode::INTERNAL_SERVER_ERROR, "response_build"),
126 E::NotAuthenticated => (StatusCode::UNAUTHORIZED, "not_authenticated"),
127 E::NoSession => (StatusCode::SERVICE_UNAVAILABLE, "no_session"),
128 E::Other(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal"),
129 _ => {
131 tracing::error!(error = ?err, "unmapped bezant::Error variant");
132 (StatusCode::INTERNAL_SERVER_ERROR, "internal")
133 }
134 }
135}