Skip to main content

structured_proxy/transcode/
metadata.rs

1//! HTTP → gRPC metadata forwarding.
2//!
3//! Converts relevant HTTP headers into gRPC `MetadataMap` entries
4//! for upstream calls. Forwarded headers are configurable via YAML.
5
6use axum::http::HeaderMap;
7use tonic::metadata::MetadataMap;
8
9/// Extract HTTP headers into a gRPC `MetadataMap`.
10/// Only forwards headers listed in `forwarded_headers`.
11pub fn http_headers_to_grpc_metadata(
12    headers: &HeaderMap,
13    forwarded_headers: &[String],
14) -> MetadataMap {
15    let mut metadata = MetadataMap::new();
16
17    for header_name in forwarded_headers {
18        if let Some(value) = headers.get(header_name.as_str()) {
19            if let Ok(meta_value) = tonic::metadata::AsciiMetadataValue::try_from(value.as_bytes())
20            {
21                if let Ok(key) =
22                    header_name.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
23                {
24                    metadata.insert(key, meta_value);
25                }
26            }
27        }
28    }
29
30    metadata
31}
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36    use axum::http::HeaderValue;
37
38    fn default_headers() -> Vec<String> {
39        vec![
40            "authorization".into(),
41            "dpop".into(),
42            "x-request-id".into(),
43            "x-forwarded-for".into(),
44            "x-forwarded-proto".into(),
45            "x-real-ip".into(),
46            "accept-language".into(),
47            "user-agent".into(),
48            "idempotency-key".into(),
49        ]
50    }
51
52    #[test]
53    fn test_authorization_forwarded() {
54        let mut headers = HeaderMap::new();
55        headers.insert("authorization", HeaderValue::from_static("Bearer tok123"));
56        let meta = http_headers_to_grpc_metadata(&headers, &default_headers());
57        assert_eq!(
58            meta.get("authorization").unwrap().to_str().unwrap(),
59            "Bearer tok123"
60        );
61    }
62
63    #[test]
64    fn test_multiple_headers_forwarded() {
65        let mut headers = HeaderMap::new();
66        headers.insert("authorization", HeaderValue::from_static("Bearer tok"));
67        headers.insert("x-request-id", HeaderValue::from_static("req-42"));
68        headers.insert("accept-language", HeaderValue::from_static("en-US"));
69        let meta = http_headers_to_grpc_metadata(&headers, &default_headers());
70        assert_eq!(
71            meta.get("authorization").unwrap().to_str().unwrap(),
72            "Bearer tok"
73        );
74        assert_eq!(
75            meta.get("x-request-id").unwrap().to_str().unwrap(),
76            "req-42"
77        );
78        assert_eq!(
79            meta.get("accept-language").unwrap().to_str().unwrap(),
80            "en-US"
81        );
82    }
83
84    #[test]
85    fn test_unknown_headers_not_forwarded() {
86        let mut headers = HeaderMap::new();
87        headers.insert("x-custom-header", HeaderValue::from_static("value"));
88        let meta = http_headers_to_grpc_metadata(&headers, &default_headers());
89        assert!(meta.get("x-custom-header").is_none());
90    }
91
92    #[test]
93    fn test_custom_forwarded_headers() {
94        let mut headers = HeaderMap::new();
95        headers.insert("x-custom-header", HeaderValue::from_static("value"));
96        let forwarded = vec!["x-custom-header".to_string()];
97        let meta = http_headers_to_grpc_metadata(&headers, &forwarded);
98        assert_eq!(
99            meta.get("x-custom-header").unwrap().to_str().unwrap(),
100            "value"
101        );
102    }
103
104    #[test]
105    fn test_empty_headers() {
106        let headers = HeaderMap::new();
107        let meta = http_headers_to_grpc_metadata(&headers, &default_headers());
108        assert!(meta.is_empty());
109    }
110
111    #[test]
112    fn test_dpop_forwarded() {
113        let mut headers = HeaderMap::new();
114        headers.insert("dpop", HeaderValue::from_static("eyJ0eXAiOiJkcG9wK2p3dCJ9"));
115        let meta = http_headers_to_grpc_metadata(&headers, &default_headers());
116        assert!(meta.get("dpop").is_some());
117    }
118}