trillium_grpc/
content_type.rs1use trillium::{Headers, KnownHeaderName};
5
6pub fn parse_grpc_content_type(value: &str) -> Option<&str> {
19 let value = value.trim();
20 let media_type = value.split(';').next().unwrap_or(value).trim();
21
22 let prefix = "application/grpc";
23 if !media_type.get(..prefix.len())?.eq_ignore_ascii_case(prefix) {
24 return None;
25 }
26
27 match &media_type[prefix.len()..] {
28 "" => Some("proto"),
29 rest if rest.starts_with('+') => Some(&rest[1..]),
30 _ => None,
31 }
32}
33
34pub fn has_te_trailers(headers: &Headers) -> bool {
41 let Some(values) = headers.get_values(KnownHeaderName::Te) else {
42 return false;
43 };
44 values
45 .iter()
46 .filter_map(|v| v.as_str())
47 .flat_map(|s| s.split(','))
48 .any(|tok| tok.trim().eq_ignore_ascii_case("trailers"))
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 #[test]
56 fn bare_application_grpc_is_proto() {
57 assert_eq!(parse_grpc_content_type("application/grpc"), Some("proto"));
58 }
59
60 #[test]
61 fn proto_suffix() {
62 assert_eq!(
63 parse_grpc_content_type("application/grpc+proto"),
64 Some("proto")
65 );
66 }
67
68 #[test]
69 fn json_suffix() {
70 assert_eq!(
71 parse_grpc_content_type("application/grpc+json"),
72 Some("json")
73 );
74 }
75
76 #[test]
77 fn case_insensitive_prefix() {
78 assert_eq!(
79 parse_grpc_content_type("Application/GRPC+proto"),
80 Some("proto")
81 );
82 }
83
84 #[test]
85 fn parameters_are_ignored() {
86 assert_eq!(
87 parse_grpc_content_type("application/grpc+proto; charset=utf-8"),
88 Some("proto")
89 );
90 assert_eq!(
91 parse_grpc_content_type("application/grpc; encoding=identity"),
92 Some("proto")
93 );
94 }
95
96 #[test]
97 fn surrounding_whitespace_trimmed() {
98 assert_eq!(
99 parse_grpc_content_type(" application/grpc+json "),
100 Some("json")
101 );
102 }
103
104 #[test]
105 fn rejects_non_grpc_types() {
106 assert_eq!(parse_grpc_content_type("application/json"), None);
107 assert_eq!(parse_grpc_content_type("text/plain"), None);
108 assert_eq!(parse_grpc_content_type(""), None);
109 assert_eq!(parse_grpc_content_type("application/grpc-web"), None);
110 assert_eq!(parse_grpc_content_type("application/grpcfoo"), None);
111 }
112
113 #[test]
114 fn te_trailers_present() {
115 let mut headers = Headers::new();
116 headers.insert(KnownHeaderName::Te, "trailers");
117 assert!(has_te_trailers(&headers));
118 }
119
120 #[test]
121 fn te_trailers_case_insensitive() {
122 let mut headers = Headers::new();
123 headers.insert(KnownHeaderName::Te, "Trailers");
124 assert!(has_te_trailers(&headers));
125 }
126
127 #[test]
128 fn te_trailers_among_other_values() {
129 let mut headers = Headers::new();
130 headers.insert(KnownHeaderName::Te, "gzip, trailers");
131 assert!(has_te_trailers(&headers));
132 }
133
134 #[test]
135 fn te_missing_or_wrong() {
136 let headers = Headers::new();
137 assert!(!has_te_trailers(&headers));
138
139 let mut headers = Headers::new();
140 headers.insert(KnownHeaderName::Te, "gzip");
141 assert!(!has_te_trailers(&headers));
142 }
143}