Skip to main content

trillium_grpc/
content_type.rs

1//! Request preflight checks: the `content-type` and `te: trailers` headers
2//! the gRPC HTTP/2 protocol requires.
3
4use trillium::{Headers, KnownHeaderName};
5
6/// Parse a gRPC content-type header.
7///
8/// Returns `Some(suffix)` if the value is a valid gRPC content-type. The bare
9/// `application/grpc` returns `Some("proto")` since the spec treats it as an
10/// alias for `application/grpc+proto`.
11///
12/// Examples:
13/// - `application/grpc` → `Some("proto")`
14/// - `application/grpc+proto` → `Some("proto")`
15/// - `application/grpc+json` → `Some("json")`
16/// - `application/grpc+json; charset=utf-8` → `Some("json")` (parameters ignored)
17/// - `application/json` → `None`
18pub 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
34/// Validate that the request carries `te: trailers`. The gRPC HTTP/2 spec
35/// requires this so HTTP/2 intermediaries don't strip response trailers.
36///
37/// Returns `true` if any value of the `te` header equals `trailers`
38/// (case-insensitive). `te` is a list-valued header, so multiple comma-separated
39/// values or multiple header lines are both accepted.
40pub 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}