Files
mnote/rust/crates/mnote-web/src/error.rs
T

151 lines
4.8 KiB
Rust
Raw Normal View History

use crate::context::RequestContext;
use axum::http::StatusCode;
use axum::http::{HeaderName, HeaderValue};
use axum::response::{IntoResponse, Response};
2026-04-29 12:24:44 +08:00
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<String>,
pub trace_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<Value>,
}
#[derive(Debug, Clone)]
pub struct WebError {
status: StatusCode,
code: &'static str,
message: String,
request_context: Option<RequestContext>,
headers: Vec<(&'static str, String)>,
details: Option<Value>,
}
impl WebError {
pub fn new(status: StatusCode, code: &'static str, message: impl Into<String>) -> Self {
Self {
status,
code,
message: message.into(),
request_context: None,
headers: Vec::new(),
details: None,
}
}
pub fn bad_request(message: impl Into<String>) -> Self {
Self::new(StatusCode::BAD_REQUEST, "bad_request", message)
}
pub fn bad_request_code(code: &'static str, message: impl Into<String>) -> Self {
Self::new(StatusCode::BAD_REQUEST, code, message)
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", message)
}
pub fn bad_gateway_code(code: &'static str, message: impl Into<String>) -> Self {
Self::new(StatusCode::BAD_GATEWAY, code, message)
}
pub fn service_unavailable_code(code: &'static str, message: impl Into<String>) -> Self {
Self::new(StatusCode::SERVICE_UNAVAILABLE, code, message)
}
pub fn gateway_timeout_code(code: &'static str, message: impl Into<String>) -> 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<String>) -> Self {
self.headers.push((name, value.into()));
self
}
2026-04-26 19:35:52 +08:00
pub fn with_details(mut self, details: Value) -> Self {
self.details = Some(details);
self
}
2026-04-26 19:35:52 +08:00
pub fn message(&self) -> &str {
&self.message
}
2026-05-19 08:49:02 +08:00
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<mnote_vault_core::VaultError> 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
}
}