use bytes::Bytes;
use super::stream::StreamHead;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct GrpcCall<'a> {
pub service: &'a str,
pub method: &'a str,
}
impl<'a> GrpcCall<'a> {
pub fn parse_path(path: &'a str) -> Option<Self> {
let rest = path.strip_prefix('/')?;
let (service, method) = rest.split_once('/')?;
if service.is_empty() || method.is_empty() || method.contains('/') {
return None;
}
Some(Self { service, method })
}
}
pub fn is_grpc_content_type(value: &[u8]) -> bool {
const PREFIX: &[u8] = b"application/grpc";
let Some(rest) = value.strip_prefix(PREFIX) else {
return false;
};
rest.is_empty() || matches!(rest.first(), Some(b'+') | Some(b';'))
}
pub fn grpc_call(head: &StreamHead) -> Option<GrpcCall<'_>> {
let content_type = head.field("content-type")?;
if !is_grpc_content_type(content_type) {
return None;
}
GrpcCall::parse_path(head.path()?)
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct GrpcStatus {
pub code: u32,
pub message: Option<Bytes>,
}
impl GrpcStatus {
pub fn is_ok(&self) -> bool {
self.code == 0
}
pub fn name(&self) -> Option<&'static str> {
Some(match self.code {
0 => "OK",
1 => "CANCELLED",
2 => "UNKNOWN",
3 => "INVALID_ARGUMENT",
4 => "DEADLINE_EXCEEDED",
5 => "NOT_FOUND",
6 => "ALREADY_EXISTS",
7 => "PERMISSION_DENIED",
8 => "RESOURCE_EXHAUSTED",
9 => "FAILED_PRECONDITION",
10 => "ABORTED",
11 => "OUT_OF_RANGE",
12 => "UNIMPLEMENTED",
13 => "INTERNAL",
14 => "UNAVAILABLE",
15 => "DATA_LOSS",
16 => "UNAUTHENTICATED",
_ => return None,
})
}
}
pub fn grpc_status(fields: &[(Bytes, Bytes)]) -> Option<GrpcStatus> {
let find = |name: &str| {
fields
.iter()
.find(|(n, _)| n.as_ref() == name.as_bytes())
.map(|(_, v)| v.clone())
};
let code = find("grpc-status")?;
let code = std::str::from_utf8(&code).ok()?.trim().parse().ok()?;
Some(GrpcStatus {
code,
message: find("grpc-message"),
})
}
pub fn grpc_status_of(head: &StreamHead) -> Option<GrpcStatus> {
grpc_status(&head.fields)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FlowSide;
fn head(fields: &[(&str, &str)], end_stream: bool) -> StreamHead {
StreamHead {
stream_id: 1,
dir: FlowSide::Initiator,
fields: fields
.iter()
.map(|(n, v)| {
(
Bytes::copy_from_slice(n.as_bytes()),
Bytes::copy_from_slice(v.as_bytes()),
)
})
.collect(),
end_stream,
}
}
#[test]
fn a_grpc_path_splits_into_service_and_method() {
let c = GrpcCall::parse_path("/routeguide.RouteGuide/GetFeature").unwrap();
assert_eq!(c.service, "routeguide.RouteGuide");
assert_eq!(c.method, "GetFeature");
}
#[test]
fn a_path_that_is_not_grpc_shaped_is_rejected() {
assert!(GrpcCall::parse_path("/onlyservice").is_none());
assert!(GrpcCall::parse_path("/a/b/c").is_none());
assert!(GrpcCall::parse_path("//Method").is_none());
assert!(GrpcCall::parse_path("/Service/").is_none());
assert!(GrpcCall::parse_path("no-leading-slash/M").is_none());
}
#[test]
fn content_type_matches_the_family_not_just_the_exact_value() {
assert!(is_grpc_content_type(b"application/grpc"));
assert!(is_grpc_content_type(b"application/grpc+proto"));
assert!(is_grpc_content_type(b"application/grpc+json"));
assert!(is_grpc_content_type(b"application/grpc; charset=utf-8"));
assert!(!is_grpc_content_type(b"application/grpcfoo"));
assert!(!is_grpc_content_type(b"application/json"));
assert!(!is_grpc_content_type(b""));
}
#[test]
fn a_grpc_stream_yields_its_call() {
let h = head(
&[
(":method", "POST"),
(":path", "/pkg.Svc/Method"),
(":authority", "grpc.example"),
("content-type", "application/grpc+proto"),
],
false,
);
let call = grpc_call(&h).expect("this is a gRPC stream");
assert_eq!(call.service, "pkg.Svc");
assert_eq!(call.method, "Method");
assert_eq!(h.authority(), Some("grpc.example"));
}
#[test]
fn a_plain_http2_stream_is_not_a_grpc_call() {
let h = head(
&[
(":method", "GET"),
(":path", "/index.html"),
("content-type", "text/html"),
],
false,
);
assert!(grpc_call(&h).is_none());
}
#[test]
fn status_comes_from_the_trailers() {
let fields = vec![
(Bytes::from_static(b"grpc-status"), Bytes::from_static(b"5")),
(
Bytes::from_static(b"grpc-message"),
Bytes::from_static(b"not found"),
),
];
let s = grpc_status(&fields).unwrap();
assert_eq!(s.code, 5);
assert_eq!(s.name(), Some("NOT_FOUND"));
assert!(!s.is_ok());
assert_eq!(s.message.as_deref(), Some(&b"not found"[..]));
}
#[test]
fn a_trailers_only_response_carries_its_status_in_the_head() {
let h = head(
&[
(":status", "200"),
("content-type", "application/grpc"),
("grpc-status", "12"),
("grpc-message", "unimplemented"),
],
true,
);
assert!(h.end_stream);
let s = grpc_status_of(&h).expect("Trailers-Only carries the status in the head");
assert_eq!(s.code, 12);
assert_eq!(s.name(), Some("UNIMPLEMENTED"));
}
#[test]
fn an_application_failure_still_looks_like_http_200() {
let h = head(
&[(":status", "200"), ("content-type", "application/grpc")],
false,
);
assert_eq!(h.status(), Some(200));
let trailers = vec![(
Bytes::from_static(b"grpc-status"),
Bytes::from_static(b"14"),
)];
let s = grpc_status(&trailers).unwrap();
assert!(!s.is_ok());
assert_eq!(s.name(), Some("UNAVAILABLE"));
}
#[test]
fn a_missing_or_unparsable_status_is_none() {
assert!(grpc_status(&[]).is_none());
let bad = vec![(
Bytes::from_static(b"grpc-status"),
Bytes::from_static(b"not-a-number"),
)];
assert!(grpc_status(&bad).is_none());
}
#[test]
fn an_unknown_status_code_has_no_name_but_still_parses() {
let fields = vec![(
Bytes::from_static(b"grpc-status"),
Bytes::from_static(b"99"),
)];
let s = grpc_status(&fields).unwrap();
assert_eq!(s.code, 99);
assert_eq!(s.name(), None);
assert!(!s.is_ok());
}
}