use axum::{extract::Request, http::HeaderMap, middleware::Next, response::Response};
use std::any::Any;
use crate::config::ConfigError;
use toolkit_canonical_errors::CanonicalError;
use toolkit_odata::Error as ODataError;
pub async fn error_mapping_middleware(request: Request, next: Next) -> Response {
next.run(request).await
}
pub fn extract_trace_id(headers: &HeaderMap) -> Option<String> {
headers
.get("x-trace-id")
.or_else(|| headers.get("x-request-id"))
.or_else(|| headers.get("traceparent"))
.and_then(|v| v.to_str().ok())
.map(str::to_owned)
.or_else(|| {
tracing::Span::current()
.id()
.map(|id| id.into_u64().to_string())
})
}
#[must_use]
pub fn map_error_to_canonical(error: &dyn Any) -> CanonicalError {
if let Some(odata_err) = error.downcast_ref::<ODataError>() {
return CanonicalError::from(odata_err.clone());
}
if let Some(config_err) = error.downcast_ref::<ConfigError>() {
let detail = match config_err {
ConfigError::GearNotFound { gear } => {
format!("Gear '{gear}' configuration not found")
}
ConfigError::InvalidGearStructure { gear } => {
format!("Gear '{gear}' has invalid configuration structure")
}
ConfigError::MissingConfigSection { gear } => {
format!("Gear '{gear}' is missing required config section")
}
ConfigError::InvalidConfig { gear, .. } => {
format!("Gear '{gear}' has invalid configuration")
}
ConfigError::VarExpand { gear, source } => {
tracing::error!(
gear = %gear,
error = %source,
"Environment variable expansion failed in gear config"
);
format!("Gear '{gear}' has invalid environment-backed configuration")
}
};
return CanonicalError::internal(detail).create();
}
if let Some(anyhow_err) = error.downcast_ref::<anyhow::Error>() {
return CanonicalError::internal(format!("{anyhow_err:#}")).create();
}
CanonicalError::internal("unknown error type in error mapping layer").create()
}
pub trait IntoCanonical {
fn into_canonical(self) -> CanonicalError;
}
impl IntoCanonical for ODataError {
fn into_canonical(self) -> CanonicalError {
CanonicalError::from(self)
}
}
impl IntoCanonical for ConfigError {
fn into_canonical(self) -> CanonicalError {
map_error_to_canonical(&self as &dyn Any)
}
}
impl IntoCanonical for anyhow::Error {
fn into_canonical(self) -> CanonicalError {
map_error_to_canonical(&self as &dyn Any)
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use toolkit_canonical_errors::Problem;
#[test]
fn odata_error_maps_to_invalid_argument() {
let canonical = ODataError::InvalidFilter("malformed".to_owned()).into_canonical();
assert_eq!(canonical.status_code(), 400);
assert!(canonical.gts_type().contains("invalid_argument"));
let problem = Problem::from(canonical);
assert_eq!(problem.status, 400);
assert!(problem.instance.is_none());
}
#[test]
fn config_gear_not_found_preserves_diagnostic() {
let canonical = ConfigError::GearNotFound {
gear: "test_gear".to_owned(),
}
.into_canonical();
assert_eq!(canonical.status_code(), 500);
assert!(canonical.gts_type().contains("internal"));
let diag = canonical.diagnostic().expect("Internal carries diagnostic");
assert!(diag.contains("test_gear"), "diagnostic was {diag:?}");
let problem = Problem::from(canonical);
assert!(
!problem.detail.contains("test_gear"),
"wire detail leaked gear name: {}",
problem.detail
);
}
#[test]
fn anyhow_error_preserves_diagnostic() {
let canonical = anyhow::anyhow!("Something went wrong").into_canonical();
assert_eq!(canonical.status_code(), 500);
let diag = canonical.diagnostic().expect("Internal carries diagnostic");
assert!(diag.contains("Something went wrong"));
}
#[test]
fn config_var_expand_redacts_env_and_source_from_diagnostic() {
let source = toolkit_utils::var_expand::ExpandVarsError::Var {
name: "SECRET_API_KEY".to_owned(),
source: std::env::VarError::NotPresent,
};
let canonical = ConfigError::VarExpand {
gear: "my_mod".to_owned(),
source,
}
.into_canonical();
assert_eq!(canonical.status_code(), 500);
let diag = canonical.diagnostic().expect("Internal carries diagnostic");
assert!(
!diag.contains("SECRET_API_KEY"),
"diagnostic leaked env var name: {diag}"
);
assert!(
!diag.contains("not present"),
"diagnostic leaked source text: {diag}"
);
assert!(
diag.contains("my_mod"),
"diagnostic dropped gear name: {diag}"
);
let problem = Problem::from(canonical);
assert!(!problem.detail.contains("SECRET_API_KEY"));
assert!(!problem.detail.contains("not present"));
assert!(!problem.detail.contains("my_mod"));
}
#[test]
fn test_extract_trace_id_from_headers() {
let mut headers = HeaderMap::new();
headers.insert("x-trace-id", "test-trace-123".parse().unwrap());
let trace_id = extract_trace_id(&headers);
assert_eq!(trace_id, Some("test-trace-123".to_owned()));
}
}