Skip to main content

rs_zero/rpc/
interceptor.rs

1use tonic::{Request, Status, service::Interceptor};
2use uuid::Uuid;
3
4/// Metadata key propagated by rs-zero RPC clients.
5pub const REQUEST_ID_METADATA: &str = "x-request-id";
6
7/// Builds an interceptor that adds a request id when missing.
8pub fn request_id_interceptor() -> impl Interceptor {
9    |mut request: Request<()>| -> Result<Request<()>, Status> {
10        if !request.metadata().contains_key(REQUEST_ID_METADATA) {
11            let request_id = Uuid::new_v4().to_string();
12            let value = request_id
13                .parse()
14                .map_err(|_| Status::internal("invalid request id metadata"))?;
15            request.metadata_mut().insert(REQUEST_ID_METADATA, value);
16        }
17
18        Ok(request)
19    }
20}
21
22/// Builds an interceptor that injects the current traceparent when available.
23#[cfg(feature = "observability")]
24pub fn trace_context_interceptor() -> impl Interceptor {
25    |mut request: Request<()>| -> Result<Request<()>, Status> {
26        if !request
27            .metadata()
28            .contains_key(crate::observability::TRACEPARENT_HEADER)
29        {
30            #[cfg(feature = "otlp")]
31            {
32                crate::observability::inject_current_context_metadata(request.metadata_mut())
33                    .map_err(|_| Status::internal("invalid traceparent metadata"))?;
34            }
35
36            #[cfg(not(feature = "otlp"))]
37            if let Some(traceparent) = crate::observability::current_traceparent() {
38                crate::observability::insert_traceparent_metadata(
39                    request.metadata_mut(),
40                    &traceparent,
41                )
42                .map_err(|_| Status::internal("invalid traceparent metadata"))?;
43            }
44        }
45
46        Ok(request)
47    }
48}
49
50/// Builds an interceptor that adds `grpc-timeout` metadata when missing.
51pub fn deadline_interceptor(timeout: std::time::Duration) -> impl Interceptor {
52    move |mut request: Request<()>| -> Result<Request<()>, Status> {
53        if !request.metadata().contains_key("grpc-timeout") {
54            crate::rpc::deadline::insert_grpc_timeout(&mut request, timeout)
55                .map_err(|_| Status::internal("invalid grpc-timeout metadata"))?;
56        }
57        Ok(request)
58    }
59}
60
61/// Builds a stable resilience key for RPC adapters.
62pub fn rpc_resilience_key(service: &str, method: &str) -> String {
63    format!("{service}:{method}")
64}
65
66/// Maps a resilience rejection into a tonic unavailable status.
67pub fn resilience_rejection_status(reason: impl std::fmt::Display) -> Status {
68    Status::unavailable(reason.to_string())
69}
70
71#[cfg(test)]
72mod tests {
73    use super::{
74        REQUEST_ID_METADATA, deadline_interceptor, request_id_interceptor,
75        resilience_rejection_status, rpc_resilience_key,
76    };
77    use tonic::{Request, service::Interceptor};
78
79    #[test]
80    fn interceptor_sets_request_id() {
81        let mut interceptor = request_id_interceptor();
82        let request = interceptor.call(Request::new(())).expect("request");
83
84        assert!(request.metadata().contains_key(REQUEST_ID_METADATA));
85    }
86
87    #[test]
88    fn interceptor_sets_grpc_timeout() {
89        let mut interceptor = deadline_interceptor(std::time::Duration::from_millis(30));
90        let request = interceptor.call(Request::new(())).expect("request");
91
92        assert!(request.metadata().contains_key("grpc-timeout"));
93    }
94
95    #[test]
96    fn rpc_resilience_helpers_are_stable() {
97        assert_eq!(rpc_resilience_key("hello", "Say"), "hello:Say");
98        assert_eq!(
99            resilience_rejection_status("open").code(),
100            tonic::Code::Unavailable
101        );
102    }
103}