use crate::context::RequestContext; use axum::http::StatusCode; use axum::http::{HeaderName, HeaderValue}; use axum::response::{IntoResponse, Response}; use axum::Json; use serde::Serialize; use serde_json::Value; #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ErrorBody { pub ok: bool, pub code: &'static str, pub message: String, pub request_id: Option, pub trace_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub details: Option, } #[derive(Debug, Clone)] pub struct WebError { status: StatusCode, code: &'static str, message: String, request_context: Option, headers: Vec<(&'static str, String)>, details: Option, } impl WebError { pub fn new(status: StatusCode, code: &'static str, message: impl Into) -> Self { Self { status, code, message: message.into(), request_context: None, headers: Vec::new(), details: None, } } pub fn bad_request(message: impl Into) -> Self { Self::new(StatusCode::BAD_REQUEST, "bad_request", message) } pub fn bad_request_code(code: &'static str, message: impl Into) -> Self { Self::new(StatusCode::BAD_REQUEST, code, message) } pub fn internal(message: impl Into) -> Self { Self::new(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", message) } pub fn bad_gateway_code(code: &'static str, message: impl Into) -> Self { Self::new(StatusCode::BAD_GATEWAY, code, message) } pub fn service_unavailable_code(code: &'static str, message: impl Into) -> Self { Self::new(StatusCode::SERVICE_UNAVAILABLE, code, message) } pub fn gateway_timeout_code(code: &'static str, message: impl Into) -> Self { Self::new(StatusCode::GATEWAY_TIMEOUT, code, message) } pub fn with_context(mut self, request_context: &RequestContext) -> Self { self.request_context = Some(request_context.clone()); self } pub fn with_header(mut self, name: &'static str, value: impl Into) -> Self { self.headers.push((name, value.into())); self } pub fn with_details(mut self, details: Value) -> Self { self.details = Some(details); self } pub fn message(&self) -> &str { &self.message } pub fn code(&self) -> &'static str { self.code } pub fn status(&self) -> StatusCode { self.status } } /// Map independent vault-core errors into HTTP WebError (12-2 P1a shared read path). impl From for WebError { fn from(err: mnote_vault_core::VaultError) -> Self { use mnote_vault_core::VaultStatus; let status = match err.status { VaultStatus::BadRequest => StatusCode::BAD_REQUEST, VaultStatus::Unauthorized => StatusCode::UNAUTHORIZED, VaultStatus::Forbidden => StatusCode::FORBIDDEN, VaultStatus::NotFound => StatusCode::NOT_FOUND, VaultStatus::Conflict => StatusCode::CONFLICT, VaultStatus::Locked => StatusCode::from_u16(423).unwrap_or(StatusCode::FORBIDDEN), VaultStatus::Unavailable => StatusCode::SERVICE_UNAVAILABLE, VaultStatus::Internal => StatusCode::INTERNAL_SERVER_ERROR, }; let mut web = WebError::new(status, err.code, err.message); if let Some(details) = err.details { web = web.with_details(details); } web } } impl IntoResponse for WebError { fn into_response(self) -> Response { let body = ErrorBody { ok: false, code: self.code, message: self.message, request_id: self .request_context .as_ref() .map(|context| context.trace.request_id.clone()), trace_id: self .request_context .as_ref() .map(|context| context.trace.trace_id.clone()), details: self.details, }; let mut response = (self.status, Json(body)).into_response(); if let Ok(name) = HeaderName::from_lowercase(b"x-error-code") { if let Ok(value) = HeaderValue::from_str(self.code) { response.headers_mut().insert(name, value); } } for (name, value) in self.headers { let Ok(header_name) = HeaderName::from_lowercase(name.as_bytes()) else { continue; }; let Ok(header_value) = HeaderValue::from_str(&value) else { continue; }; response.headers_mut().insert(header_name, header_value); } response } }