use axum::{ Json, http::{HeaderMap, StatusCode}, response::IntoResponse, }; use serde::Serialize; #[derive(Debug, thiserror::Error)] pub enum AppError { #[error("bad request: {0}")] BadRequest(String), #[error("pokemon not found")] NotFound, #[error("upstream service returned invalid data: {0}")] InvalidUpstreamData(String), #[error("upstream service failed: {0}")] Upstream(String), #[error("request timed out")] Timeout, #[error("internal server error")] Internal, } impl AppError { #[must_use] pub const fn status_code(&self) -> StatusCode { match self { Self::BadRequest(_) => StatusCode::BAD_REQUEST, Self::NotFound => StatusCode::NOT_FOUND, Self::InvalidUpstreamData(_) | Self::Upstream(_) | Self::Timeout => { StatusCode::BAD_GATEWAY } Self::Internal => StatusCode::INTERNAL_SERVER_ERROR, } } #[must_use] pub const fn code(&self) -> &'static str { match self { Self::BadRequest(_) => "bad_request", Self::NotFound => "pokemon_not_found", Self::InvalidUpstreamData(_) => "upstream_invalid_data", Self::Upstream(_) | Self::Timeout => "upstream_unavailable", Self::Internal => "internal_error", } } #[must_use] pub fn public_message(&self) -> String { match self { Self::BadRequest(message) => message.clone(), Self::NotFound => "pokemon not found".to_owned(), Self::InvalidUpstreamData(_) => "upstream service returned invalid data".to_owned(), Self::Upstream(_) | Self::Timeout => "upstream service failed".to_owned(), Self::Internal => "internal server error".to_owned(), } } } impl IntoResponse for AppError { fn into_response(self) -> axum::response::Response { ApiError::new(self, None).into_response() } } pub struct ApiError { source: AppError, request_id: Option, } impl ApiError { #[must_use] pub const fn new(source: AppError, request_id: Option) -> Self { Self { source, request_id } } #[must_use] pub fn from_headers(source: AppError, headers: &HeaderMap) -> Self { Self::new(source, request_id_from_headers(headers)) } } impl IntoResponse for ApiError { fn into_response(self) -> axum::response::Response { let status = self.source.status_code(); let body = ErrorBody::new( self.source.code(), self.source.public_message(), self.request_id, ); (status, Json(body)).into_response() } } #[derive(Serialize)] pub struct ErrorBody { code: &'static str, message: String, #[serde(rename = "requestId", skip_serializing_if = "Option::is_none")] request_id: Option, } impl ErrorBody { #[must_use] pub fn new(code: &'static str, message: impl Into, request_id: Option) -> Self { Self { code, message: message.into(), request_id, } } } #[must_use] pub fn request_id_from_headers(headers: &HeaderMap) -> Option { headers .get("x-request-id") .and_then(|value| value.to_str().ok()) .map(str::to_owned) }