use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct ErrorBody {
pub code: &'static str,
pub message: String,
}
#[derive(Debug)]
pub struct AppError(bezant::Error);
impl AppError {
#[must_use]
pub fn inner(&self) -> &bezant::Error {
&self.0
}
}
impl From<bezant::Error> for AppError {
fn from(value: bezant::Error) -> Self {
Self(value)
}
}
impl From<anyhow::Error> for AppError {
fn from(value: anyhow::Error) -> Self {
Self(bezant::Error::from(value))
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, code) = map_status(&self.0);
if status.is_server_error() {
tracing::error!(
code,
status = %status,
error = %self.0,
"request failed (5xx)"
);
} else {
tracing::warn!(
code,
status = %status,
error = %self.0,
"request failed (4xx)"
);
}
let body = ErrorBody {
code,
message: self.0.to_string(),
};
(status, Json(body)).into_response()
}
}
fn map_status(err: &bezant::Error) -> (StatusCode, &'static str) {
use bezant::Error as E;
match err {
E::InvalidBaseUrl(_) => (StatusCode::BAD_REQUEST, "invalid_base_url"),
E::UrlNotABase { .. } => (StatusCode::BAD_REQUEST, "url_not_a_base"),
E::Http(e) => {
if e.is_timeout() {
(StatusCode::GATEWAY_TIMEOUT, "upstream_timeout")
} else if e.is_connect() {
(StatusCode::SERVICE_UNAVAILABLE, "upstream_unreachable")
} else {
(StatusCode::BAD_GATEWAY, "upstream_http_error")
}
}
E::UpstreamStatus { status, .. } => {
let s = StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_GATEWAY);
if s.is_server_error() || s.as_u16() == 429 {
(s, "upstream_status")
} else if s.is_client_error() {
(s, "upstream_client_error")
} else {
(StatusCode::BAD_GATEWAY, "upstream_status")
}
}
E::Unknown { .. } => (StatusCode::BAD_GATEWAY, "upstream_unknown_variant"),
E::Decode { .. } => (StatusCode::BAD_GATEWAY, "upstream_decode_error"),
E::Api(_) => (StatusCode::BAD_GATEWAY, "upstream_api_error"),
E::BadRequest(_) => (StatusCode::BAD_REQUEST, "bad_request"),
E::MissingQuery { .. } => (StatusCode::BAD_REQUEST, "missing_query_param"),
E::Header { .. } => (StatusCode::BAD_REQUEST, "invalid_header_value"),
E::SymbolNotFound { .. } => (StatusCode::NOT_FOUND, "symbol_not_found"),
E::BadConid { .. } => (StatusCode::BAD_GATEWAY, "upstream_bad_conid"),
E::WsHandshake { .. } => (StatusCode::BAD_GATEWAY, "ws_handshake_failed"),
E::WsTransport { .. } => (StatusCode::BAD_GATEWAY, "ws_transport_failed"),
E::WsProtocol(_) => (StatusCode::BAD_GATEWAY, "ws_protocol_error"),
E::ResponseBuild(_) => (StatusCode::INTERNAL_SERVER_ERROR, "response_build"),
E::NotAuthenticated => (StatusCode::UNAUTHORIZED, "not_authenticated"),
E::NoSession => (StatusCode::SERVICE_UNAVAILABLE, "no_session"),
E::Other(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal"),
_ => {
tracing::error!(error = ?err, "unmapped bezant::Error variant");
(StatusCode::INTERNAL_SERVER_ERROR, "internal")
}
}
}