rskit_server/
error_layer.rs1use std::future::Future;
9use std::pin::Pin;
10use std::task::{Context, Poll};
11
12use http::{Request, Response};
13use tonic::body::Body;
14use tower::{Layer, Service};
15
16use rskit_errors::{AppError, ProblemDetail};
17use rskit_grpc::grpc_code_to_error_code;
18
19#[derive(Debug, Clone, Default)]
24pub struct ErrorLayer;
25
26impl ErrorLayer {
27 pub fn new() -> Self {
29 Self
30 }
31}
32
33impl<S> Layer<S> for ErrorLayer {
34 type Service = ErrorService<S>;
35
36 fn layer(&self, inner: S) -> Self::Service {
37 ErrorService { inner }
38 }
39}
40
41#[derive(Debug, Clone)]
49pub struct ErrorService<S> {
50 inner: S,
51}
52
53impl<S: tonic::server::NamedService> tonic::server::NamedService for ErrorService<S> {
54 const NAME: &'static str = S::NAME;
55}
56
57impl<S> Service<Request<Body>> for ErrorService<S>
58where
59 S: Service<Request<Body>, Response = Response<Body>> + Clone + Send + 'static,
60 S::Future: Send + 'static,
61 S::Error: Send + 'static,
62{
63 type Response = S::Response;
64 type Error = S::Error;
65 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
66
67 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
68 self.inner.poll_ready(cx)
69 }
70
71 fn call(&mut self, req: Request<Body>) -> Self::Future {
72 let mut inner = self.inner.clone();
73 Box::pin(async move {
74 let response = inner.call(req).await?;
75 Ok(enrich_error_response(response))
76 })
77 }
78}
79
80fn enrich_error_response(response: Response<Body>) -> Response<Body> {
84 let status_code = response
86 .headers()
87 .get("grpc-status")
88 .and_then(|v| v.to_str().ok())
89 .and_then(|v| v.parse::<i32>().ok())
90 .unwrap_or(0);
91
92 if status_code == 0 {
93 return response;
94 }
95
96 if response.headers().contains_key("grpc-status-details-bin") {
99 return response;
100 }
101
102 let grpc_message = response
104 .headers()
105 .get("grpc-message")
106 .and_then(|v| v.to_str().ok())
107 .unwrap_or("unknown error")
108 .to_string();
109
110 let code = tonic::Code::from_i32(status_code);
111 let error_code = grpc_code_to_error_code(code);
112 let app_err = AppError::new(error_code, grpc_message);
113 let problem = ProblemDetail::from(&app_err);
114
115 if let Ok(json_bytes) = serde_json::to_vec(&problem) {
116 let encoded = base64_encode(&json_bytes);
117 let (mut parts, body) = response.into_parts();
118 if let Ok(val) = http::HeaderValue::from_str(&encoded) {
119 parts.headers.insert("grpc-status-details-bin", val);
120 }
121 Response::from_parts(parts, body)
122 } else {
123 response
124 }
125}
126
127fn base64_encode(data: &[u8]) -> String {
129 use base64::Engine;
130 base64::engine::general_purpose::STANDARD.encode(data)
131}
132
133#[cfg(test)]
136mod tests {
137 use super::*;
138 use rskit_errors::ErrorCode;
139
140 #[test]
141 fn enrich_skips_ok_response() {
142 let resp = Response::builder()
143 .header("grpc-status", "0")
144 .body(Body::default())
145 .unwrap();
146 let result = enrich_error_response(resp);
147 assert!(!result.headers().contains_key("grpc-status-details-bin"));
148 }
149
150 #[test]
151 fn enrich_skips_when_details_already_present() {
152 let resp = Response::builder()
153 .header("grpc-status", "5")
154 .header("grpc-status-details-bin", "existing")
155 .body(Body::default())
156 .unwrap();
157 let result = enrich_error_response(resp);
158 assert_eq!(
159 result.headers().get("grpc-status-details-bin").unwrap(),
160 "existing"
161 );
162 }
163
164 #[test]
165 fn enrich_adds_problem_detail_for_error_without_details() {
166 let resp = Response::builder()
167 .header("grpc-status", "5")
168 .header("grpc-message", "user not found")
169 .body(Body::default())
170 .unwrap();
171 let result = enrich_error_response(resp);
172 assert!(result.headers().contains_key("grpc-status-details-bin"));
173
174 let encoded = result
175 .headers()
176 .get("grpc-status-details-bin")
177 .unwrap()
178 .to_str()
179 .unwrap();
180 use base64::Engine;
181 let decoded = base64::engine::general_purpose::STANDARD
182 .decode(encoded)
183 .unwrap();
184 let pd: ProblemDetail = serde_json::from_slice(&decoded).unwrap();
185 assert_eq!(pd.code, ErrorCode::NotFound);
186 assert_eq!(pd.detail, "user not found");
187 assert_eq!(pd.status, 404);
188 assert!(pd.error_type.ends_with("not-found"));
189 }
190
191 #[test]
192 fn enrich_handles_missing_grpc_status_header() {
193 let resp = Response::builder().body(Body::default()).unwrap();
194 let result = enrich_error_response(resp);
195 assert!(!result.headers().contains_key("grpc-status-details-bin"));
196 }
197
198 #[test]
199 fn error_layer_default_and_new_are_equivalent() {
200 let _l1 = ErrorLayer;
201 let _l2 = ErrorLayer::new();
202 }
203
204 #[tokio::test]
205 async fn layer_wraps_service_and_enriches_error_response() {
206 use tower::ServiceExt;
207
208 let inner = tower::service_fn(|_req: Request<Body>| async {
209 Ok::<_, std::convert::Infallible>(
210 Response::builder()
211 .header("grpc-status", "14")
212 .header("grpc-message", "backend unavailable")
213 .body(Body::default())
214 .unwrap(),
215 )
216 });
217 let service = ErrorLayer::new().layer(inner);
218
219 let result = service
220 .oneshot(Request::new(Body::default()))
221 .await
222 .expect("service response");
223
224 assert!(result.headers().contains_key("grpc-status-details-bin"));
225 }
226}