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