http_tunnel_common/utils/
headers.rs

1use http::{HeaderMap, HeaderName, HeaderValue};
2use std::collections::HashMap;
3
4/// Convert HTTP headers to our internal format
5/// Supports multiple values per header name
6pub fn headers_to_map(headers: &HeaderMap) -> HashMap<String, Vec<String>> {
7    let mut map: HashMap<String, Vec<String>> = HashMap::new();
8
9    for (name, value) in headers.iter() {
10        let key = name.as_str().to_string();
11        let val = value.to_str().unwrap_or("").to_string();
12
13        map.entry(key).or_default().push(val);
14    }
15
16    map
17}
18
19/// Convert our internal header format to HTTP HeaderMap
20pub fn map_to_headers(map: &HashMap<String, Vec<String>>) -> HeaderMap {
21    let mut headers = HeaderMap::new();
22
23    for (name, values) in map.iter() {
24        if let Ok(header_name) = HeaderName::from_bytes(name.as_bytes()) {
25            for value in values {
26                if let Ok(header_value) = HeaderValue::from_str(value) {
27                    headers.append(header_name.clone(), header_value);
28                }
29            }
30        }
31    }
32
33    headers
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn test_headers_to_map_empty() {
42        let headers = HeaderMap::new();
43        let map = headers_to_map(&headers);
44        assert!(map.is_empty());
45    }
46
47    #[test]
48    fn test_headers_to_map_single() {
49        let mut headers = HeaderMap::new();
50        headers.insert("content-type", "application/json".parse().unwrap());
51
52        let map = headers_to_map(&headers);
53        assert_eq!(map.len(), 1);
54        assert_eq!(map.get("content-type").unwrap(), &vec!["application/json"]);
55    }
56
57    #[test]
58    fn test_headers_to_map_multiple() {
59        let mut headers = HeaderMap::new();
60        headers.insert("content-type", "application/json".parse().unwrap());
61        headers.insert("authorization", "Bearer token123".parse().unwrap());
62        headers.insert("x-custom-header", "custom-value".parse().unwrap());
63
64        let map = headers_to_map(&headers);
65        assert_eq!(map.len(), 3);
66        assert_eq!(map.get("content-type").unwrap(), &vec!["application/json"]);
67        assert_eq!(map.get("authorization").unwrap(), &vec!["Bearer token123"]);
68        assert_eq!(map.get("x-custom-header").unwrap(), &vec!["custom-value"]);
69    }
70
71    #[test]
72    fn test_headers_to_map_multiple_values() {
73        let mut headers = HeaderMap::new();
74        headers.insert("set-cookie", "session=abc".parse().unwrap());
75        headers.append("set-cookie", "token=xyz".parse().unwrap());
76
77        let map = headers_to_map(&headers);
78        assert_eq!(map.len(), 1);
79
80        let cookies = map.get("set-cookie").unwrap();
81        assert_eq!(cookies.len(), 2);
82        assert!(cookies.contains(&"session=abc".to_string()));
83        assert!(cookies.contains(&"token=xyz".to_string()));
84    }
85
86    #[test]
87    fn test_map_to_headers_empty() {
88        let map: HashMap<String, Vec<String>> = HashMap::new();
89        let headers = map_to_headers(&map);
90        assert!(headers.is_empty());
91    }
92
93    #[test]
94    fn test_map_to_headers_single() {
95        let mut map = HashMap::new();
96        map.insert(
97            "content-type".to_string(),
98            vec!["application/json".to_string()],
99        );
100
101        let headers = map_to_headers(&map);
102        assert_eq!(headers.len(), 1);
103        assert_eq!(headers.get("content-type").unwrap(), "application/json");
104    }
105
106    #[test]
107    fn test_map_to_headers_multiple() {
108        let mut map = HashMap::new();
109        map.insert("content-type".to_string(), vec!["text/plain".to_string()]);
110        map.insert("host".to_string(), vec!["example.com".to_string()]);
111        map.insert("user-agent".to_string(), vec!["test-agent".to_string()]);
112
113        let headers = map_to_headers(&map);
114        assert_eq!(headers.len(), 3);
115        assert_eq!(headers.get("content-type").unwrap(), "text/plain");
116        assert_eq!(headers.get("host").unwrap(), "example.com");
117        assert_eq!(headers.get("user-agent").unwrap(), "test-agent");
118    }
119
120    #[test]
121    fn test_map_to_headers_multiple_values() {
122        let mut map = HashMap::new();
123        map.insert(
124            "set-cookie".to_string(),
125            vec!["session=abc".to_string(), "token=xyz".to_string()],
126        );
127
128        let headers = map_to_headers(&map);
129
130        // get_all returns an iterator over all values for a header
131        let cookies: Vec<_> = headers
132            .get_all("set-cookie")
133            .iter()
134            .map(|v| v.to_str().unwrap())
135            .collect();
136
137        assert_eq!(cookies.len(), 2);
138        assert!(cookies.contains(&"session=abc"));
139        assert!(cookies.contains(&"token=xyz"));
140    }
141
142    #[test]
143    fn test_roundtrip_conversion() {
144        let mut original = HeaderMap::new();
145        original.insert("content-type", "application/json".parse().unwrap());
146        original.insert("authorization", "Bearer token".parse().unwrap());
147        original.insert("x-request-id", "req-123".parse().unwrap());
148
149        // Convert to map and back
150        let map = headers_to_map(&original);
151        let converted = map_to_headers(&map);
152
153        // Verify all headers preserved
154        assert_eq!(converted.len(), original.len());
155        assert_eq!(
156            converted.get("content-type").unwrap(),
157            original.get("content-type").unwrap()
158        );
159        assert_eq!(
160            converted.get("authorization").unwrap(),
161            original.get("authorization").unwrap()
162        );
163        assert_eq!(
164            converted.get("x-request-id").unwrap(),
165            original.get("x-request-id").unwrap()
166        );
167    }
168
169    #[test]
170    fn test_roundtrip_with_multiple_values() {
171        let mut original = HeaderMap::new();
172        original.insert("accept", "text/html".parse().unwrap());
173        original.append("accept", "application/json".parse().unwrap());
174        original.insert("cookie", "session=abc".parse().unwrap());
175        original.append("cookie", "token=xyz".parse().unwrap());
176
177        let map = headers_to_map(&original);
178        let converted = map_to_headers(&map);
179
180        // Check accept header
181        let accept_values: Vec<_> = converted
182            .get_all("accept")
183            .iter()
184            .map(|v| v.to_str().unwrap())
185            .collect();
186        assert_eq!(accept_values.len(), 2);
187        assert!(accept_values.contains(&"text/html"));
188        assert!(accept_values.contains(&"application/json"));
189
190        // Check cookie header
191        let cookie_values: Vec<_> = converted
192            .get_all("cookie")
193            .iter()
194            .map(|v| v.to_str().unwrap())
195            .collect();
196        assert_eq!(cookie_values.len(), 2);
197        assert!(cookie_values.contains(&"session=abc"));
198        assert!(cookie_values.contains(&"token=xyz"));
199    }
200
201    #[test]
202    fn test_map_to_headers_invalid_header_name() {
203        let mut map = HashMap::new();
204        map.insert("valid-header".to_string(), vec!["value".to_string()]);
205        map.insert("invalid header".to_string(), vec!["value".to_string()]); // Space is invalid
206
207        let headers = map_to_headers(&map);
208
209        // Only valid header should be included
210        assert_eq!(headers.len(), 1);
211        assert!(headers.get("valid-header").is_some());
212        assert!(headers.get("invalid header").is_none());
213    }
214
215    #[test]
216    fn test_headers_to_map_non_utf8_handling() {
217        let mut headers = HeaderMap::new();
218        headers.insert("content-type", "application/json".parse().unwrap());
219
220        // Add a header with non-UTF8 value (though this is rare in practice)
221        // HeaderValue allows non-UTF8 values
222        let non_utf8_value = HeaderValue::from_bytes(&[0xFF, 0xFE]).unwrap();
223        headers.insert("x-binary-header", non_utf8_value);
224
225        let map = headers_to_map(&headers);
226
227        // Non-UTF8 header should result in empty string
228        assert_eq!(map.get("x-binary-header").unwrap(), &vec![""]);
229        assert_eq!(map.get("content-type").unwrap(), &vec!["application/json"]);
230    }
231}