toolkit/api/
error_layer.rs1use axum::{extract::Request, middleware::Next, response::Response};
9use std::any::Any;
10
11use crate::config::ConfigError;
12use toolkit_canonical_errors::CanonicalError;
13use toolkit_odata::Error as ODataError;
14
15pub async fn error_mapping_middleware(request: Request, next: Next) -> Response {
19 next.run(request).await
20}
21
22#[must_use]
30pub fn map_error_to_canonical(error: &dyn Any) -> CanonicalError {
31 if let Some(odata_err) = error.downcast_ref::<ODataError>() {
32 return CanonicalError::from(odata_err.clone());
33 }
34
35 if let Some(config_err) = error.downcast_ref::<ConfigError>() {
36 let detail = match config_err {
37 ConfigError::GearNotFound { gear } => {
38 format!("Gear '{gear}' configuration not found")
39 }
40 ConfigError::InvalidGearStructure { gear } => {
41 format!("Gear '{gear}' has invalid configuration structure")
42 }
43 ConfigError::MissingConfigSection { gear } => {
44 format!("Gear '{gear}' is missing required config section")
45 }
46 ConfigError::InvalidConfig { gear, .. } => {
47 format!("Gear '{gear}' has invalid configuration")
48 }
49 ConfigError::VarExpand { gear, cause } => {
50 tracing::error!(
56 gear = %gear,
57 error = %cause,
58 "Environment variable expansion failed in gear config"
59 );
60 format!("Gear '{gear}' has invalid environment-backed configuration")
61 }
62 };
63 return CanonicalError::internal(detail).create();
64 }
65
66 if let Some(anyhow_err) = error.downcast_ref::<anyhow::Error>() {
67 return CanonicalError::internal(format!("{anyhow_err:#}")).create();
68 }
69
70 CanonicalError::internal("unknown error type in error mapping layer").create()
71}
72
73pub trait IntoCanonical {
79 fn into_canonical(self) -> CanonicalError;
80}
81
82impl IntoCanonical for ODataError {
83 fn into_canonical(self) -> CanonicalError {
84 CanonicalError::from(self)
85 }
86}
87
88impl IntoCanonical for ConfigError {
89 fn into_canonical(self) -> CanonicalError {
90 map_error_to_canonical(&self as &dyn Any)
91 }
92}
93
94impl IntoCanonical for anyhow::Error {
95 fn into_canonical(self) -> CanonicalError {
96 map_error_to_canonical(&self as &dyn Any)
97 }
98}
99
100#[cfg(test)]
101#[cfg_attr(coverage_nightly, coverage(off))]
102mod tests {
103 use super::*;
104 use toolkit_canonical_errors::Problem;
105
106 #[test]
107 fn odata_error_maps_to_invalid_argument() {
108 let canonical = ODataError::InvalidFilter("malformed".to_owned()).into_canonical();
109 assert_eq!(canonical.status_code(), 400);
110 assert!(canonical.gts_type().contains("invalid_argument"));
111
112 let problem = Problem::from(canonical);
114 assert_eq!(problem.status, Some(400));
115 assert!(problem.instance.is_none());
116 }
117
118 #[test]
119 fn config_gear_not_found_preserves_diagnostic() {
120 let canonical = ConfigError::GearNotFound {
121 gear: "test_gear".to_owned(),
122 }
123 .into_canonical();
124
125 assert_eq!(canonical.status_code(), 500);
126 assert!(canonical.gts_type().contains("internal"));
127
128 let diag = canonical.diagnostic().expect("Internal carries diagnostic");
131 assert!(diag.contains("test_gear"), "diagnostic was {diag:?}");
132
133 let problem = Problem::from(canonical);
134 assert!(
135 !problem.detail.contains("test_gear"),
136 "wire detail leaked gear name: {}",
137 problem.detail
138 );
139 }
140
141 #[test]
142 fn anyhow_error_preserves_diagnostic() {
143 let canonical = anyhow::anyhow!("Something went wrong").into_canonical();
144 assert_eq!(canonical.status_code(), 500);
145 let diag = canonical.diagnostic().expect("Internal carries diagnostic");
146 assert!(diag.contains("Something went wrong"));
147 }
148
149 #[test]
150 fn config_var_expand_redacts_env_and_source_from_diagnostic() {
151 let source = toolkit_utils::var_expand::ExpandVarsError::Var {
152 name: "SECRET_API_KEY".to_owned(),
153 source: std::env::VarError::NotPresent,
154 };
155 let canonical = ConfigError::VarExpand {
156 gear: "my_mod".to_owned(),
157 cause: source,
158 }
159 .into_canonical();
160
161 assert_eq!(canonical.status_code(), 500);
162
163 let diag = canonical.diagnostic().expect("Internal carries diagnostic");
167 assert!(
168 !diag.contains("SECRET_API_KEY"),
169 "diagnostic leaked env var name: {diag}"
170 );
171 assert!(
172 !diag.contains("not present"),
173 "diagnostic leaked source text: {diag}"
174 );
175 assert!(
176 diag.contains("my_mod"),
177 "diagnostic dropped gear name: {diag}"
178 );
179
180 let problem = Problem::from(canonical);
181 assert!(!problem.detail.contains("SECRET_API_KEY"));
182 assert!(!problem.detail.contains("not present"));
183 assert!(!problem.detail.contains("my_mod"));
184 }
185}