Skip to main content

grpc_webnext/
transcode.rs

1//! JSON <-> protobuf transcoding for the `+json` codec.
2//!
3//! grpc-webnext carries opaque message bytes; the envelope (frames, trailers)
4//! is always protobuf, but the *application message* may be JSON. Converting
5//! JSON to the binary protobuf that a gRPC handler expects (and back) needs the
6//! message descriptors, so a `Transcoder` is built from a compiled
7//! `FileDescriptorSet` (e.g. `protoc --descriptor_set_out` /
8//! `prost_build ... file_descriptor_set_path`).
9
10use prost_reflect::prost::Message;
11use prost_reflect::{DescriptorPool, DynamicMessage, MessageDescriptor};
12use serde::Serialize;
13
14use crate::httprule::HttpRouter;
15pub use crate::httprule::{HttpCall, WsBinding};
16
17#[derive(Debug, thiserror::Error)]
18pub enum TranscodeError {
19    #[error("failed to load descriptor set: {0}")]
20    Descriptor(String),
21    #[error("unknown method: {0}")]
22    UnknownMethod(String),
23    #[error("json error: {0}")]
24    Json(#[from] serde_json::Error),
25    #[error("protobuf decode error: {0}")]
26    Decode(String),
27    #[error("http transcoding: {0}")]
28    Http(String),
29}
30
31/// Transcodes application messages between JSON and binary protobuf, keyed by
32/// gRPC method path (`/pkg.Service/Method`), and maps `google.api.http` REST
33/// bindings onto gRPC methods.
34#[derive(Clone)]
35pub struct Transcoder {
36    pool: DescriptorPool,
37    router: HttpRouter,
38}
39
40impl Transcoder {
41    /// Build from an encoded `FileDescriptorSet`.
42    pub fn from_file_descriptor_set(bytes: &[u8]) -> Result<Self, TranscodeError> {
43        let pool = DescriptorPool::decode(bytes)
44            .map_err(|e| TranscodeError::Descriptor(e.to_string()))?;
45        let router = HttpRouter::from_pool(&pool);
46        Ok(Self { pool, router })
47    }
48
49    /// Whether any `google.api.http` REST bindings were found.
50    pub fn has_http_rules(&self) -> bool {
51        !self.router.is_empty()
52    }
53
54    /// Map a REST request `(method, path, query, body)` onto a gRPC call, or
55    /// `Ok(None)` if no HTTP binding matches.
56    pub fn transcode_http_request(
57        &self,
58        method: &str,
59        path: &str,
60        query: Option<&str>,
61        body: &[u8],
62    ) -> Result<Option<HttpCall>, TranscodeError> {
63        self.router.transcode(method, path, query, body)
64    }
65
66    /// Resolve a WebSocket annotation route from its upgrade path, or `None` if the
67    /// path matches no binding.
68    pub fn match_ws(&self, path: &str, query: Option<&str>) -> Option<WsBinding> {
69        self.router.match_ws(path, query)
70    }
71
72    /// Whether a gRPC method has any HTTP annotation (used to keep an annotated
73    /// RPC's main route off the plain-HTTP surface).
74    pub fn is_annotated_method(&self, grpc_method: &str) -> bool {
75        self.router.is_annotated(grpc_method)
76    }
77
78    /// Resolve `(request_type, response_type)` for a method path.
79    fn io_types(&self, path: &str) -> Result<(MessageDescriptor, MessageDescriptor), TranscodeError> {
80        let (service, method) = path
81            .trim_start_matches('/')
82            .split_once('/')
83            .ok_or_else(|| TranscodeError::UnknownMethod(path.to_string()))?;
84        let svc = self
85            .pool
86            .get_service_by_name(service)
87            .ok_or_else(|| TranscodeError::UnknownMethod(path.to_string()))?;
88        let m = svc
89            .methods()
90            .find(|m| m.name() == method)
91            .ok_or_else(|| TranscodeError::UnknownMethod(path.to_string()))?;
92        Ok((m.input(), m.output()))
93    }
94
95    /// Whether `path` (`/pkg.Service/Method`) resolves to a known method — i.e. this
96    /// transcoder can handle it. Callers use it to distinguish "unknown method"
97    /// (UNIMPLEMENTED) from a genuine transcode/validation failure.
98    pub fn has_method(&self, path: &str) -> bool {
99        self.io_types(path).is_ok()
100    }
101
102    /// JSON request message -> binary protobuf. Empty input is the default message.
103    pub fn request_json_to_proto(&self, path: &str, json: &[u8]) -> Result<Vec<u8>, TranscodeError> {
104        let (input, _) = self.io_types(path)?;
105        Ok(self.json_to_proto(input, json)?.encode_to_vec())
106    }
107
108    /// Binary protobuf response message -> JSON.
109    pub fn response_proto_to_json(&self, path: &str, proto: &[u8]) -> Result<Vec<u8>, TranscodeError> {
110        let (_, output) = self.io_types(path)?;
111        self.proto_to_json(output, proto)
112    }
113
114    fn json_to_proto(
115        &self,
116        desc: MessageDescriptor,
117        json: &[u8],
118    ) -> Result<DynamicMessage, TranscodeError> {
119        if json.is_empty() {
120            return Ok(DynamicMessage::new(desc));
121        }
122        let mut de = serde_json::Deserializer::from_slice(json);
123        let msg = DynamicMessage::deserialize(desc, &mut de)?;
124        de.end()?;
125        Ok(msg)
126    }
127
128    fn proto_to_json(&self, desc: MessageDescriptor, proto: &[u8]) -> Result<Vec<u8>, TranscodeError> {
129        let msg =
130            DynamicMessage::decode(desc, proto).map_err(|e| TranscodeError::Decode(e.to_string()))?;
131        let mut buf = Vec::new();
132        let mut ser = serde_json::Serializer::new(&mut buf);
133        msg.serialize(&mut ser)?;
134        Ok(buf)
135    }
136}