use aide::OperationOutput; use axum::{ http::{StatusCode, header}, response::{IntoResponse, Response}, }; use brk_error::Error as BrkError; use schemars::JsonSchema; use serde::Serialize; use crate::{cache::CacheParams, etag::Etag}; /// Server result type with Error that implements IntoResponse. pub type Result = std::result::Result; const DOC_URL: &str = "/api"; #[derive(Serialize, JsonSchema)] pub(crate) struct ErrorBody { error: ErrorDetail, } #[derive(Serialize, JsonSchema)] struct ErrorDetail { /// Error category: "invalid_request", "forbidden", "not_found", "unavailable", or "internal" #[schemars(with = "String")] r#type: &'static str, /// Machine-readable error code (e.g. "invalid_addr", "series_not_found") #[schemars(with = "String")] code: &'static str, /// Human-readable description message: String, /// Link to API documentation #[schemars(with = "String")] doc_url: &'static str, } fn error_type(status: StatusCode) -> &'static str { match status { StatusCode::BAD_REQUEST => "invalid_request", StatusCode::FORBIDDEN => "forbidden", StatusCode::NOT_FOUND => "not_found", StatusCode::SERVICE_UNAVAILABLE => "unavailable", _ => "internal", } } fn error_status(e: &BrkError) -> StatusCode { match e { BrkError::InvalidTxid | BrkError::InvalidNetwork | BrkError::InvalidAddr | BrkError::UnsupportedType(_) | BrkError::Parse(_) | BrkError::NoSeries | BrkError::SeriesUnsupportedIndex { .. } | BrkError::WeightExceeded { .. } | BrkError::TooManyUtxos => StatusCode::BAD_REQUEST, BrkError::UnknownAddr | BrkError::UnknownTxid | BrkError::NotFound(_) | BrkError::NoData | BrkError::OutOfRange(_) | BrkError::UnindexableDate | BrkError::SeriesNotFound(_) => StatusCode::NOT_FOUND, BrkError::AuthFailed => StatusCode::FORBIDDEN, BrkError::MempoolNotAvailable => StatusCode::SERVICE_UNAVAILABLE, _ => StatusCode::INTERNAL_SERVER_ERROR, } } fn error_code(e: &BrkError) -> &'static str { match e { BrkError::InvalidAddr => "invalid_addr", BrkError::InvalidTxid => "invalid_txid", BrkError::InvalidNetwork => "invalid_network", BrkError::UnsupportedType(_) => "unsupported_type", BrkError::Parse(_) => "parse_error", BrkError::NoSeries => "no_series", BrkError::SeriesUnsupportedIndex { .. } => "series_unsupported_index", BrkError::WeightExceeded { .. } => "weight_exceeded", BrkError::TooManyUtxos => "too_many_utxos", BrkError::UnknownAddr => "unknown_addr", BrkError::UnknownTxid => "unknown_txid", BrkError::NotFound(_) => "not_found", BrkError::OutOfRange(_) => "out_of_range", BrkError::UnindexableDate => "unindexable_date", BrkError::NoData => "no_data", BrkError::SeriesNotFound(_) => "series_not_found", BrkError::MempoolNotAvailable => "mempool_not_available", BrkError::AuthFailed => "auth_failed", _ => "internal_error", } } fn build_error_body(status: StatusCode, code: &'static str, message: String) -> Vec { serde_json::to_vec(&ErrorBody { error: ErrorDetail { r#type: error_type(status), code, message, doc_url: DOC_URL, }, }) .unwrap() } /// Server error type that maps to HTTP status codes and structured JSON. pub struct Error { status: StatusCode, code: &'static str, message: String, } impl Error { pub(crate) fn new(status: StatusCode, code: &'static str, msg: impl Into) -> Self { Self { status, code, message: msg.into(), } } pub fn bad_request(msg: impl Into) -> Self { Self::new(StatusCode::BAD_REQUEST, "bad_request", msg) } pub fn forbidden(msg: impl Into) -> Self { Self::new(StatusCode::FORBIDDEN, "forbidden", msg) } pub fn not_found(msg: impl Into) -> Self { Self::new(StatusCode::NOT_FOUND, "not_found", msg) } pub fn not_implemented(msg: impl Into) -> Self { Self::new(StatusCode::NOT_IMPLEMENTED, "not_implemented", msg) } pub fn internal(msg: impl Into) -> Self { Self::new(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", msg) } pub(crate) fn into_response_with_etag(self, etag: Etag) -> Response { let params = CacheParams::error(etag); let body = build_error_body(self.status, self.code, self.message); let mut response = ( self.status, [(header::CONTENT_TYPE, "application/problem+json")], body, ) .into_response(); params.apply_to(response.headers_mut()); response } } impl From for Error { fn from(e: BrkError) -> Self { Self { status: error_status(&e), code: error_code(&e), message: e.to_string(), } } } impl OperationOutput for Error { type Inner = (); } impl IntoResponse for Error { fn into_response(self) -> Response { let body = build_error_body(self.status, self.code, self.message); let mut response = ( self.status, [(header::CONTENT_TYPE, "application/problem+json")], body, ) .into_response(); CacheParams::apply_error_cache_control(response.headers_mut()); response } }