modkit/api/
error_layer.rs1use axum::{extract::Request, http::HeaderMap, middleware::Next, response::Response};
8use http::StatusCode;
9use std::any::Any;
10
11use crate::api::problem::Problem;
12use crate::config::ConfigError;
13use modkit_odata::Error as ODataError;
14
15pub async fn error_mapping_middleware(request: Request, next: Next) -> Response {
21 let _uri = request.uri().clone();
22 let _headers = request.headers().clone();
23
24 let response = next.run(request).await;
25
26 if response.status().is_success() || is_problem_response(&response) {
28 return response;
29 }
30
31 response
35}
36
37fn is_problem_response(response: &Response) -> bool {
39 response
40 .headers()
41 .get(axum::http::header::CONTENT_TYPE)
42 .and_then(|v| v.to_str().ok())
43 .is_some_and(|ct| ct.contains("application/problem+json"))
44}
45
46pub fn extract_trace_id(headers: &HeaderMap) -> Option<String> {
48 headers
50 .get("x-trace-id")
51 .or_else(|| headers.get("x-request-id"))
52 .or_else(|| headers.get("traceparent"))
53 .and_then(|v| v.to_str().ok())
54 .map(ToString::to_string)
55 .or_else(|| {
56 tracing::Span::current()
58 .id()
59 .map(|id| id.into_u64().to_string())
60 })
61}
62
63pub fn map_error_to_problem(error: &dyn Any, instance: &str, trace_id: Option<String>) -> Problem {
68 if let Some(odata_err) = error.downcast_ref::<ODataError>() {
70 return crate::api::odata::error::odata_error_to_problem(odata_err, instance, trace_id);
71 }
72
73 if let Some(config_err) = error.downcast_ref::<ConfigError>() {
74 let mut problem = match config_err {
75 ConfigError::ModuleNotFound { module } => Problem::new(
76 StatusCode::INTERNAL_SERVER_ERROR,
77 "Configuration Error",
78 format!("Module '{module}' configuration not found"),
79 )
80 .with_code("CONFIG_MODULE_NOT_FOUND")
81 .with_type("https://errors.example.com/CONFIG_MODULE_NOT_FOUND"),
82
83 ConfigError::InvalidModuleStructure { module } => Problem::new(
84 StatusCode::INTERNAL_SERVER_ERROR,
85 "Configuration Error",
86 format!("Module '{module}' has invalid configuration structure"),
87 )
88 .with_code("CONFIG_INVALID_STRUCTURE")
89 .with_type("https://errors.example.com/CONFIG_INVALID_STRUCTURE"),
90
91 ConfigError::MissingConfigSection { module } => Problem::new(
92 StatusCode::INTERNAL_SERVER_ERROR,
93 "Configuration Error",
94 format!("Module '{module}' is missing required config section"),
95 )
96 .with_code("CONFIG_MISSING_SECTION")
97 .with_type("https://errors.example.com/CONFIG_MISSING_SECTION"),
98
99 ConfigError::InvalidConfig { module, .. } => Problem::new(
100 StatusCode::INTERNAL_SERVER_ERROR,
101 "Configuration Error",
102 format!("Module '{module}' has invalid configuration"),
103 )
104 .with_code("CONFIG_INVALID")
105 .with_type("https://errors.example.com/CONFIG_INVALID"),
106 };
107
108 problem = problem.with_instance(instance);
109 if let Some(tid) = trace_id {
110 problem = problem.with_trace_id(tid);
111 }
112 return problem;
113 }
114
115 if let Some(anyhow_err) = error.downcast_ref::<anyhow::Error>() {
117 let mut problem = Problem::new(
118 StatusCode::INTERNAL_SERVER_ERROR,
119 "Internal Server Error",
120 "An internal error occurred",
121 )
122 .with_code("INTERNAL_ERROR")
123 .with_type("https://errors.example.com/INTERNAL_ERROR");
124
125 problem = problem.with_instance(instance);
126 if let Some(tid) = trace_id {
127 problem = problem.with_trace_id(tid);
128 }
129
130 tracing::error!(error = %anyhow_err, "Internal server error");
132 return problem;
133 }
134
135 let mut problem = Problem::new(
137 StatusCode::INTERNAL_SERVER_ERROR,
138 "Unknown Error",
139 "An unknown error occurred",
140 )
141 .with_code("UNKNOWN_ERROR")
142 .with_type("https://errors.example.com/UNKNOWN_ERROR");
143
144 problem = problem.with_instance(instance);
145 if let Some(tid) = trace_id {
146 problem = problem.with_trace_id(tid);
147 }
148
149 tracing::error!("Unknown error type in error mapping layer");
150 problem
151}
152
153pub trait IntoProblem {
155 fn into_problem(self, instance: &str, trace_id: Option<String>) -> Problem;
156}
157
158impl IntoProblem for ODataError {
159 fn into_problem(self, instance: &str, trace_id: Option<String>) -> Problem {
160 crate::api::odata::error::odata_error_to_problem(&self, instance, trace_id)
161 }
162}
163
164impl IntoProblem for ConfigError {
165 fn into_problem(self, instance: &str, trace_id: Option<String>) -> Problem {
166 map_error_to_problem(&self as &dyn Any, instance, trace_id)
167 }
168}
169
170impl IntoProblem for anyhow::Error {
171 fn into_problem(self, instance: &str, trace_id: Option<String>) -> Problem {
172 map_error_to_problem(&self as &dyn Any, instance, trace_id)
173 }
174}
175
176#[cfg(test)]
177#[cfg_attr(coverage_nightly, coverage(off))]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_odata_error_mapping() {
183 let error = ODataError::InvalidFilter("malformed".to_owned());
184 let problem = error.into_problem("/tests/v1/test", Some("trace123".to_owned()));
185
186 assert_eq!(problem.status, StatusCode::UNPROCESSABLE_ENTITY);
187 assert!(problem.code.contains("invalid_filter"));
188 assert_eq!(problem.instance, "/tests/v1/test");
189 assert_eq!(problem.trace_id, Some("trace123".to_owned()));
190 }
191
192 #[test]
193 fn test_config_error_mapping() {
194 let error = ConfigError::ModuleNotFound {
195 module: "test_module".to_owned(),
196 };
197 let problem = error.into_problem("/tests/v1/test", None);
198
199 assert_eq!(problem.status, StatusCode::INTERNAL_SERVER_ERROR);
200 assert_eq!(problem.code, "CONFIG_MODULE_NOT_FOUND");
201 assert_eq!(problem.instance, "/tests/v1/test");
202 assert!(problem.detail.contains("test_module"));
203 }
204
205 #[test]
206 fn test_anyhow_error_mapping() {
207 let error = anyhow::anyhow!("Something went wrong");
208 let problem = error.into_problem("/tests/v1/test", Some("trace456".to_owned()));
209
210 assert_eq!(problem.status, StatusCode::INTERNAL_SERVER_ERROR);
211 assert_eq!(problem.code, "INTERNAL_ERROR");
212 assert_eq!(problem.instance, "/tests/v1/test");
213 assert_eq!(problem.trace_id, Some("trace456".to_owned()));
214 }
215
216 #[test]
217 fn test_extract_trace_id_from_headers() {
218 let mut headers = HeaderMap::new();
219 headers.insert("x-trace-id", "test-trace-123".parse().unwrap());
220
221 let trace_id = extract_trace_id(&headers);
222 assert_eq!(trace_id, Some("test-trace-123".to_owned()));
223 }
224}