Skip to main content

gateway/
transcode.rs

1//! Descriptor-driven JSON/HTTP to gRPC transcoding.
2
3use actix_web::{
4    error::ErrorInternalServerError,
5    http::{header, Method, StatusCode},
6    web, HttpRequest, HttpResponse,
7};
8use futures::TryStreamExt;
9use http::uri::PathAndQuery;
10use prost::Message;
11use prost_reflect::{DescriptorPool, DynamicMessage, Kind, MessageDescriptor, MethodDescriptor};
12use std::{collections::BTreeMap, fmt, sync::Arc};
13use tonic::{
14    client::Grpc,
15    codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder},
16    metadata::{AsciiMetadataKey, AsciiMetadataValue},
17    transport::Channel,
18    Code, Request, Status,
19};
20
21/// HTTP verbs supported by protobuf HTTP bindings.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum HttpVerb {
24    Get,
25    Put,
26    Post,
27    Delete,
28    Patch,
29}
30
31impl HttpVerb {
32    fn matches(self, method: &Method) -> bool {
33        matches!(
34            (self, method.as_str()),
35            (Self::Get, "GET")
36                | (Self::Put, "PUT")
37                | (Self::Post, "POST")
38                | (Self::Delete, "DELETE")
39                | (Self::Patch, "PATCH")
40        )
41    }
42}
43
44/// An explicit HTTP binding for a fully-qualified protobuf method.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct HttpBinding {
47    pub verb: HttpVerb,
48    pub path: String,
49    pub rpc: String,
50    pub body: Option<String>,
51}
52
53impl HttpBinding {
54    pub fn new(verb: HttpVerb, path: impl Into<String>, rpc: impl Into<String>) -> Self {
55        Self {
56            verb,
57            path: path.into(),
58            rpc: rpc.into(),
59            body: None,
60        }
61    }
62
63    /// Maps the complete JSON body (`"*"`) or one request field to the HTTP body.
64    pub fn with_body(mut self, field: impl Into<String>) -> Self {
65        self.body = Some(field.into());
66        self
67    }
68}
69
70#[derive(Clone)]
71struct Route {
72    binding: HttpBinding,
73    method: MethodDescriptor,
74}
75
76/// Builds a descriptor-driven transcoder from a compiled `FileDescriptorSet`.
77pub struct TranscoderBuilder {
78    pool: DescriptorPool,
79    channel: Channel,
80    bindings: Vec<HttpBinding>,
81}
82
83impl TranscoderBuilder {
84    pub fn from_descriptor_set(
85        bytes: impl AsRef<[u8]>,
86        channel: Channel,
87    ) -> Result<Self, TranscodeError> {
88        let pool = DescriptorPool::decode(bytes.as_ref()).map_err(TranscodeError::Descriptor)?;
89        Ok(Self {
90            pool,
91            channel,
92            bindings: Vec::new(),
93        })
94    }
95
96    /// Loads all service descriptors advertised by the standard gRPC v1 reflection service.
97    pub async fn from_reflection(channel: Channel) -> Result<Self, TranscodeError> {
98        use tonic_reflection::pb::v1::{
99            server_reflection_client::ServerReflectionClient,
100            server_reflection_request::MessageRequest, server_reflection_response::MessageResponse,
101            ServerReflectionRequest,
102        };
103
104        async fn request(
105            client: &mut ServerReflectionClient<Channel>,
106            message_request: MessageRequest,
107        ) -> Result<MessageResponse, TranscodeError> {
108            let response = client
109                .server_reflection_info(futures::stream::iter([ServerReflectionRequest {
110                    host: String::new(),
111                    message_request: Some(message_request),
112                }]))
113                .await
114                .map_err(|status| TranscodeError::Status(Box::new(status)))?;
115            response
116                .into_inner()
117                .message()
118                .await
119                .map_err(|status| TranscodeError::Status(Box::new(status)))?
120                .and_then(|response| response.message_response)
121                .ok_or(TranscodeError::InvalidReflectionResponse)
122        }
123
124        let mut client = ServerReflectionClient::new(channel.clone());
125        let services =
126            match request(&mut client, MessageRequest::ListServices(String::new())).await? {
127                MessageResponse::ListServicesResponse(response) => response.service,
128                MessageResponse::ErrorResponse(error) => {
129                    return Err(TranscodeError::Reflection(error.error_message));
130                }
131                _ => return Err(TranscodeError::InvalidReflectionResponse),
132            };
133        let mut files = BTreeMap::new();
134        for service in services {
135            if service.name.starts_with("grpc.reflection.") {
136                continue;
137            }
138            match request(
139                &mut client,
140                MessageRequest::FileContainingSymbol(service.name),
141            )
142            .await?
143            {
144                MessageResponse::FileDescriptorResponse(response) => {
145                    for bytes in response.file_descriptor_proto {
146                        let file = prost_types::FileDescriptorProto::decode(bytes.as_slice())
147                            .map_err(TranscodeError::ReflectionDescriptor)?;
148                        files.insert(file.name.clone().unwrap_or_default(), file);
149                    }
150                }
151                MessageResponse::ErrorResponse(error) => {
152                    return Err(TranscodeError::Reflection(error.error_message));
153                }
154                _ => return Err(TranscodeError::InvalidReflectionResponse),
155            }
156        }
157        let pool = DescriptorPool::from_file_descriptor_set(prost_types::FileDescriptorSet {
158            file: files.into_values().collect(),
159        })
160        .map_err(TranscodeError::Descriptor)?;
161        Ok(Self {
162            pool,
163            channel,
164            bindings: Vec::new(),
165        })
166    }
167
168    pub fn add_binding(mut self, binding: HttpBinding) -> Self {
169        self.bindings.push(binding);
170        self
171    }
172
173    /// Loads `google.api.http` primary and additional bindings when those extensions are present
174    /// in the descriptor set. Explicit bindings may be mixed with annotated bindings.
175    pub fn load_annotated_bindings(mut self) -> Self {
176        let Some(extension) = self.pool.get_extension_by_name("google.api.http") else {
177            return self;
178        };
179        for service in self.pool.services() {
180            for method in service.methods() {
181                let options = method.options();
182                if !options.has_extension(&extension) {
183                    continue;
184                }
185                let value = options.get_extension(&extension);
186                let Some(rule) = value.as_message() else {
187                    continue;
188                };
189                collect_http_rules(rule, method.full_name(), &mut self.bindings);
190            }
191        }
192        self
193    }
194
195    pub fn build(self) -> Result<Transcoder, TranscodeError> {
196        let mut routes = Vec::with_capacity(self.bindings.len());
197        for binding in self.bindings {
198            validate_template(&binding.path)?;
199            let method = find_method(&self.pool, &binding.rpc)
200                .ok_or_else(|| TranscodeError::UnknownMethod(binding.rpc.clone()))?;
201            if method.is_client_streaming() {
202                return Err(TranscodeError::UnsupportedClientStreaming(binding.rpc));
203            }
204            routes.push(Route { binding, method });
205        }
206        routes.sort_by_key(|route| std::cmp::Reverse(literal_weight(&route.binding.path)));
207        Ok(Transcoder {
208            channel: self.channel,
209            routes: Arc::new(routes),
210        })
211    }
212}
213
214/// A cloneable Actix handler state that dynamically invokes generated-independent gRPC methods.
215#[derive(Clone)]
216pub struct Transcoder {
217    channel: Channel,
218    routes: Arc<Vec<Route>>,
219}
220
221impl Transcoder {
222    pub async fn handle(&self, request: HttpRequest, body: web::Bytes) -> HttpResponse {
223        match self.invoke(&request, &body).await {
224            Ok(response) => response,
225            Err(TranscodeError::NoRoute) => HttpResponse::NotFound().json(error_json(
226                Code::NotFound,
227                "no HTTP-to-gRPC binding matched the request",
228            )),
229            Err(TranscodeError::InvalidJson(error)) => HttpResponse::BadRequest()
230                .json(error_json(Code::InvalidArgument, &error.to_string())),
231            Err(TranscodeError::Status(status)) => status_response(*status),
232            Err(error) => HttpResponse::InternalServerError()
233                .json(error_json(Code::Internal, &error.to_string())),
234        }
235    }
236
237    async fn invoke(
238        &self,
239        request: &HttpRequest,
240        body: &[u8],
241    ) -> Result<HttpResponse, TranscodeError> {
242        let (route, captures) = self
243            .routes
244            .iter()
245            .filter(|route| route.binding.verb.matches(request.method()))
246            .find_map(|route| {
247                match_template(&route.binding.path, request.path()).map(|c| (route, c))
248            })
249            .ok_or(TranscodeError::NoRoute)?;
250
251        let input = request_message(route, request, body, captures)?;
252        let mut grpc_request = Request::new(input);
253        forward_metadata(request, &mut grpc_request);
254        let path: PathAndQuery = format!(
255            "/{}/{}",
256            route.method.parent_service().full_name(),
257            route.method.name()
258        )
259        .parse()
260        .expect("protobuf method names form a valid gRPC path");
261        let codec = DynamicCodec::new(route.method.input(), route.method.output());
262        let mut grpc = Grpc::new(self.channel.clone());
263        grpc.ready().await.map_err(|error| {
264            TranscodeError::Status(Box::new(Status::unavailable(format!(
265                "gRPC transport unavailable: {error}"
266            ))))
267        })?;
268
269        if route.method.is_server_streaming() {
270            let response = grpc
271                .server_streaming(grpc_request, path, codec)
272                .await
273                .map_err(|status| TranscodeError::Status(Box::new(status)))?;
274            let metadata = response.metadata().clone();
275            let stream =
276                response
277                    .into_inner()
278                    .map_err(status_to_actix)
279                    .and_then(|message| async move {
280                        let mut json =
281                            serde_json::to_vec(&message).map_err(ErrorInternalServerError)?;
282                        json.push(b'\n');
283                        Ok::<_, actix_web::Error>(web::Bytes::from(json))
284                    });
285            let mut response = HttpResponse::Ok();
286            response.insert_header((header::CONTENT_TYPE, "application/json"));
287            copy_response_metadata(&metadata, &mut response);
288            return Ok(response.streaming(stream));
289        }
290
291        let response = grpc
292            .unary(grpc_request, path, codec)
293            .await
294            .map_err(|status| TranscodeError::Status(Box::new(status)))?;
295        let metadata = response.metadata().clone();
296        let json = serde_json::to_vec(response.get_ref()).map_err(TranscodeError::InvalidJson)?;
297        let mut downstream = HttpResponse::Ok();
298        downstream.insert_header((header::CONTENT_TYPE, "application/json"));
299        copy_response_metadata(&metadata, &mut downstream);
300        Ok(downstream.body(json))
301    }
302}
303
304/// Actix handler for a [`Transcoder`] stored in `web::Data`.
305pub async fn transcode(
306    transcoder: web::Data<Transcoder>,
307    request: HttpRequest,
308    body: web::Bytes,
309) -> HttpResponse {
310    transcoder.handle(request, body).await
311}
312
313fn request_message(
314    route: &Route,
315    request: &HttpRequest,
316    body: &[u8],
317    captures: BTreeMap<String, String>,
318) -> Result<DynamicMessage, TranscodeError> {
319    let mut value = if body.is_empty() {
320        serde_json::Value::Object(Default::default())
321    } else {
322        serde_json::from_slice(body).map_err(TranscodeError::InvalidJson)?
323    };
324    if let Some(field) = route.binding.body.as_deref() {
325        if field != "*" {
326            let mut root = serde_json::Map::new();
327            insert_json_path(&mut root, field, value);
328            value = serde_json::Value::Object(root);
329        }
330    } else if !body.is_empty() {
331        return Err(TranscodeError::UnexpectedBody);
332    }
333    let object = value
334        .as_object_mut()
335        .ok_or_else(|| TranscodeError::InvalidRequest("request JSON must be an object".into()))?;
336    for (name, raw) in captures {
337        insert_typed_value(object, &route.method.input(), &name, raw)?;
338    }
339    for pair in request
340        .query_string()
341        .split('&')
342        .filter(|pair| !pair.is_empty())
343    {
344        let (name, raw) = pair.split_once('=').unwrap_or((pair, ""));
345        let name = percent_decode(name)?;
346        let raw = percent_decode(raw)?;
347        insert_typed_value(object, &route.method.input(), &name, raw)?;
348    }
349    let serialized = serde_json::to_vec(&value).map_err(TranscodeError::InvalidJson)?;
350    let mut deserializer = serde_json::Deserializer::from_slice(&serialized);
351    DynamicMessage::deserialize(route.method.input(), &mut deserializer)
352        .map_err(TranscodeError::InvalidJson)
353}
354
355fn insert_typed_value(
356    object: &mut serde_json::Map<String, serde_json::Value>,
357    descriptor: &MessageDescriptor,
358    path: &str,
359    raw: String,
360) -> Result<(), TranscodeError> {
361    let top = path.split('.').next().unwrap_or(path);
362    let field = descriptor
363        .get_field_by_name(top)
364        .or_else(|| descriptor.fields().find(|field| field.json_name() == top))
365        .ok_or_else(|| TranscodeError::InvalidRequest(format!("unknown request field {path}")))?;
366    let value = match field.kind() {
367        Kind::Bool => serde_json::Value::Bool(raw.parse().map_err(|_| {
368            TranscodeError::InvalidRequest(format!("field {path} must be a boolean"))
369        })?),
370        Kind::Int32 | Kind::Sint32 | Kind::Sfixed32 => serde_json::Value::Number(
371            raw.parse::<i32>()
372                .map_err(|_| {
373                    TranscodeError::InvalidRequest(format!("field {path} must be an integer"))
374                })?
375                .into(),
376        ),
377        Kind::Uint32 | Kind::Fixed32 => serde_json::Value::Number(
378            raw.parse::<u32>()
379                .map_err(|_| {
380                    TranscodeError::InvalidRequest(format!(
381                        "field {path} must be an unsigned integer"
382                    ))
383                })?
384                .into(),
385        ),
386        _ => serde_json::Value::String(raw),
387    };
388    insert_json_path(object, path, value);
389    Ok(())
390}
391
392fn insert_json_path(
393    object: &mut serde_json::Map<String, serde_json::Value>,
394    path: &str,
395    value: serde_json::Value,
396) {
397    let mut parts = path.split('.').peekable();
398    let mut current = object;
399    while let Some(part) = parts.next() {
400        if parts.peek().is_none() {
401            current.insert(part.to_owned(), value);
402            return;
403        }
404        current = current
405            .entry(part)
406            .or_insert_with(|| serde_json::Value::Object(Default::default()))
407            .as_object_mut()
408            .expect("path collisions are rejected by protobuf JSON decoding");
409    }
410}
411
412fn forward_metadata(request: &HttpRequest, grpc: &mut Request<DynamicMessage>) {
413    for (name, value) in request.headers() {
414        let name = name.as_str();
415        if matches!(
416            name,
417            "host" | "content-type" | "content-length" | "connection" | "transfer-encoding"
418        ) || name.ends_with("-bin")
419        {
420            continue;
421        }
422        if let (Ok(key), Ok(value)) = (
423            name.parse::<AsciiMetadataKey>(),
424            AsciiMetadataValue::try_from(value.as_bytes()),
425        ) {
426            grpc.metadata_mut().append(key, value);
427        }
428    }
429}
430
431fn copy_response_metadata(
432    metadata: &tonic::metadata::MetadataMap,
433    response: &mut actix_web::HttpResponseBuilder,
434) {
435    for entry in metadata.iter() {
436        if let tonic::metadata::KeyAndValueRef::Ascii(key, value) = entry {
437            if let (Ok(name), Ok(value)) = (
438                header::HeaderName::try_from(key.as_str()),
439                header::HeaderValue::from_bytes(value.as_encoded_bytes()),
440            ) {
441                response.append_header((name, value));
442            }
443        }
444    }
445}
446
447fn collect_http_rules(rule: &DynamicMessage, rpc: &str, bindings: &mut Vec<HttpBinding>) {
448    let verb_and_path = [
449        ("get", HttpVerb::Get),
450        ("put", HttpVerb::Put),
451        ("post", HttpVerb::Post),
452        ("delete", HttpVerb::Delete),
453        ("patch", HttpVerb::Patch),
454    ]
455    .into_iter()
456    .find_map(|(field, verb)| {
457        rule.get_field_by_name(field).and_then(|value| {
458            value
459                .as_str()
460                .filter(|value| !value.is_empty())
461                .map(|path| (verb, path.to_owned()))
462        })
463    });
464    if let Some((verb, path)) = verb_and_path {
465        let body = rule.get_field_by_name("body").and_then(|value| {
466            value
467                .as_str()
468                .filter(|value| !value.is_empty())
469                .map(str::to_owned)
470        });
471        bindings.push(HttpBinding {
472            verb,
473            path,
474            rpc: rpc.to_owned(),
475            body,
476        });
477    }
478    let additional_bindings = rule.get_field_by_name("additional_bindings");
479    if let Some(rules) = additional_bindings
480        .as_deref()
481        .and_then(|value| value.as_list())
482    {
483        for rule in rules.iter().filter_map(|value| value.as_message()) {
484            collect_http_rules(rule, rpc, bindings);
485        }
486    }
487}
488
489fn find_method(pool: &DescriptorPool, name: &str) -> Option<MethodDescriptor> {
490    let normalized = name.trim_start_matches('/').replace('/', ".");
491    let (service, method) = normalized.rsplit_once('.')?;
492    pool.get_service_by_name(service)?
493        .methods()
494        .find(|candidate| candidate.name() == method)
495}
496
497fn validate_template(template: &str) -> Result<(), TranscodeError> {
498    if !template.starts_with('/')
499        || template
500            .split('/')
501            .any(|part| part.contains('{') != part.contains('}'))
502    {
503        return Err(TranscodeError::InvalidTemplate(template.to_owned()));
504    }
505    Ok(())
506}
507
508fn literal_weight(template: &str) -> usize {
509    template
510        .split('/')
511        .filter(|part| !part.starts_with('{'))
512        .map(str::len)
513        .sum()
514}
515
516fn match_template(template: &str, path: &str) -> Option<BTreeMap<String, String>> {
517    let template: Vec<_> = template.trim_matches('/').split('/').collect();
518    let path: Vec<_> = path.trim_matches('/').split('/').collect();
519    if template.len() != path.len() {
520        return None;
521    }
522    let mut captures = BTreeMap::new();
523    for (expected, actual) in template.into_iter().zip(path) {
524        if let Some(name) = expected
525            .strip_prefix('{')
526            .and_then(|part| part.strip_suffix('}'))
527        {
528            let name = name.split('=').next().unwrap_or(name);
529            captures.insert(name.to_owned(), percent_decode(actual).ok()?);
530        } else if expected != actual {
531            return None;
532        }
533    }
534    Some(captures)
535}
536
537fn percent_decode(value: &str) -> Result<String, TranscodeError> {
538    let mut bytes = Vec::with_capacity(value.len());
539    let input = value.as_bytes();
540    let mut index = 0;
541    while index < input.len() {
542        match input[index] {
543            b'%' if index + 2 < input.len() => {
544                let hex = std::str::from_utf8(&input[index + 1..index + 3]).map_err(|_| {
545                    TranscodeError::InvalidRequest("invalid percent encoding".into())
546                })?;
547                bytes.push(u8::from_str_radix(hex, 16).map_err(|_| {
548                    TranscodeError::InvalidRequest("invalid percent encoding".into())
549                })?);
550                index += 3;
551            }
552            b'+' => {
553                bytes.push(b' ');
554                index += 1;
555            }
556            byte => {
557                bytes.push(byte);
558                index += 1;
559            }
560        }
561    }
562    String::from_utf8(bytes)
563        .map_err(|_| TranscodeError::InvalidRequest("path/query value is not UTF-8".into()))
564}
565
566/// Maps canonical gRPC status codes to their HTTP equivalents.
567pub fn grpc_status_to_http(code: Code) -> StatusCode {
568    match code {
569        Code::Ok => StatusCode::OK,
570        Code::Cancelled => StatusCode::from_u16(499).expect("499 is a valid extension status"),
571        Code::Unknown | Code::Internal | Code::DataLoss => StatusCode::INTERNAL_SERVER_ERROR,
572        Code::InvalidArgument | Code::FailedPrecondition | Code::OutOfRange => {
573            StatusCode::BAD_REQUEST
574        }
575        Code::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
576        Code::NotFound => StatusCode::NOT_FOUND,
577        Code::AlreadyExists | Code::Aborted => StatusCode::CONFLICT,
578        Code::PermissionDenied => StatusCode::FORBIDDEN,
579        Code::Unauthenticated => StatusCode::UNAUTHORIZED,
580        Code::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
581        Code::Unimplemented => StatusCode::NOT_IMPLEMENTED,
582        Code::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
583    }
584}
585
586fn status_response(status: Status) -> HttpResponse {
587    HttpResponse::build(grpc_status_to_http(status.code()))
588        .json(error_json(status.code(), status.message()))
589}
590
591fn error_json(code: Code, message: &str) -> serde_json::Value {
592    serde_json::json!({
593        "code": code as i32,
594        "status": format!("{code:?}").to_ascii_uppercase(),
595        "message": message,
596    })
597}
598
599fn status_to_actix(status: Status) -> actix_web::Error {
600    ErrorInternalServerError(status.to_string())
601}
602
603#[derive(Clone)]
604struct DynamicCodec {
605    output: MessageDescriptor,
606}
607
608impl DynamicCodec {
609    fn new(_input: MessageDescriptor, output: MessageDescriptor) -> Self {
610        Self { output }
611    }
612}
613
614impl Codec for DynamicCodec {
615    type Encode = DynamicMessage;
616    type Decode = DynamicMessage;
617    type Encoder = DynamicEncoder;
618    type Decoder = DynamicDecoder;
619
620    fn encoder(&mut self) -> Self::Encoder {
621        DynamicEncoder
622    }
623
624    fn decoder(&mut self) -> Self::Decoder {
625        DynamicDecoder(self.output.clone())
626    }
627}
628
629struct DynamicEncoder;
630
631impl Encoder for DynamicEncoder {
632    type Item = DynamicMessage;
633    type Error = Status;
634
635    fn encode(&mut self, item: Self::Item, dst: &mut EncodeBuf<'_>) -> Result<(), Self::Error> {
636        item.encode(dst).map_err(|error| {
637            Status::internal(format!("failed to encode protobuf request: {error}"))
638        })
639    }
640}
641
642struct DynamicDecoder(MessageDescriptor);
643
644impl Decoder for DynamicDecoder {
645    type Item = DynamicMessage;
646    type Error = Status;
647
648    fn decode(&mut self, src: &mut DecodeBuf<'_>) -> Result<Option<Self::Item>, Self::Error> {
649        DynamicMessage::decode(self.0.clone(), src)
650            .map(Some)
651            .map_err(|error| {
652                Status::internal(format!("failed to decode protobuf response: {error}"))
653            })
654    }
655}
656
657/// Configuration, JSON conversion, route, or upstream failures from the transcoder.
658#[derive(Debug)]
659pub enum TranscodeError {
660    Descriptor(prost_reflect::DescriptorError),
661    ReflectionDescriptor(prost::DecodeError),
662    Reflection(String),
663    InvalidReflectionResponse,
664    UnknownMethod(String),
665    UnsupportedClientStreaming(String),
666    InvalidTemplate(String),
667    NoRoute,
668    UnexpectedBody,
669    InvalidRequest(String),
670    InvalidJson(serde_json::Error),
671    Status(Box<Status>),
672}
673
674impl fmt::Display for TranscodeError {
675    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
676        match self {
677            Self::Descriptor(error) => {
678                write!(formatter, "invalid protobuf descriptor set: {error}")
679            }
680            Self::ReflectionDescriptor(error) => {
681                write!(formatter, "invalid reflected protobuf descriptor: {error}")
682            }
683            Self::Reflection(message) => write!(formatter, "gRPC reflection failed: {message}"),
684            Self::InvalidReflectionResponse => {
685                formatter.write_str("gRPC reflection returned an unexpected response")
686            }
687            Self::UnknownMethod(method) => write!(formatter, "unknown protobuf method: {method}"),
688            Self::UnsupportedClientStreaming(method) => write!(
689                formatter,
690                "HTTP transcoding does not support client-streaming method {method}"
691            ),
692            Self::InvalidTemplate(path) => write!(formatter, "invalid HTTP path template: {path}"),
693            Self::NoRoute => formatter.write_str("no HTTP binding matched"),
694            Self::UnexpectedBody => {
695                formatter.write_str("this HTTP binding does not accept a request body")
696            }
697            Self::InvalidRequest(message) => formatter.write_str(message),
698            Self::InvalidJson(error) => write!(formatter, "invalid protobuf JSON: {error}"),
699            Self::Status(status) => write!(formatter, "gRPC request failed: {status}"),
700        }
701    }
702}
703
704impl std::error::Error for TranscodeError {}
705
706#[cfg(test)]
707mod tests {
708    use super::*;
709    use actix_web::{test, App};
710    use std::time::Duration;
711    use tokio::net::TcpListener;
712    use tokio_stream::wrappers::TcpListenerStream;
713    use tonic::{transport::Server, Response};
714
715    mod fixture {
716        tonic::include_proto!("rust_zero.gateway_test");
717    }
718
719    use fixture::{
720        greeter_server::{Greeter, GreeterServer},
721        GetRequest, GetResponse,
722    };
723
724    #[derive(Default)]
725    struct GreeterService;
726
727    #[tonic::async_trait]
728    impl Greeter for GreeterService {
729        type WatchStream = tokio_stream::wrappers::ReceiverStream<Result<GetResponse, Status>>;
730
731        async fn get(&self, request: Request<GetRequest>) -> Result<Response<GetResponse>, Status> {
732            let request = request.into_inner();
733            let mut response = Response::new(GetResponse {
734                id: request.id,
735                message: request.view,
736            });
737            response
738                .metadata_mut()
739                .insert("x-backend", "grpc".parse().unwrap());
740            Ok(response)
741        }
742
743        async fn watch(
744            &self,
745            request: Request<GetRequest>,
746        ) -> Result<Response<Self::WatchStream>, Status> {
747            let request = request.into_inner();
748            let (sender, receiver) = tokio::sync::mpsc::channel(2);
749            sender
750                .send(Ok(GetResponse {
751                    id: request.id,
752                    message: "one".into(),
753                }))
754                .await
755                .unwrap();
756            sender
757                .send(Ok(GetResponse {
758                    id: request.id,
759                    message: "two".into(),
760                }))
761                .await
762                .unwrap();
763            Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new(
764                receiver,
765            )))
766        }
767
768        async fn fail(&self, _: Request<GetRequest>) -> Result<Response<GetResponse>, Status> {
769            Err(Status::not_found("missing greeter"))
770        }
771    }
772
773    async fn fixture() -> (Transcoder, tokio::task::JoinHandle<()>) {
774        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
775        let address = listener.local_addr().unwrap();
776        let reflection = tonic_reflection::server::Builder::configure()
777            .register_encoded_file_descriptor_set(include_bytes!(concat!(
778                env!("OUT_DIR"),
779                "/gateway.bin"
780            )))
781            .build_v1()
782            .unwrap();
783        let server = tokio::spawn(async move {
784            Server::builder()
785                .add_service(reflection)
786                .add_service(GreeterServer::new(GreeterService))
787                .serve_with_incoming(TcpListenerStream::new(listener))
788                .await
789                .unwrap();
790        });
791        let channel = tonic::transport::Endpoint::from_shared(format!("http://{address}"))
792            .unwrap()
793            .connect_timeout(Duration::from_secs(1))
794            .connect()
795            .await
796            .unwrap();
797        let transcoder = TranscoderBuilder::from_descriptor_set(
798            include_bytes!(concat!(env!("OUT_DIR"), "/gateway.bin")),
799            channel,
800        )
801        .unwrap()
802        .add_binding(HttpBinding::new(
803            HttpVerb::Get,
804            "/v1/greeters/{id}",
805            "rust_zero.gateway_test.Greeter.Get",
806        ))
807        .add_binding(HttpBinding::new(
808            HttpVerb::Get,
809            "/v1/greeters/{id}/watch",
810            "rust_zero.gateway_test.Greeter.Watch",
811        ))
812        .add_binding(HttpBinding::new(
813            HttpVerb::Get,
814            "/v1/missing/{id}",
815            "rust_zero.gateway_test.Greeter.Fail",
816        ))
817        .build()
818        .unwrap();
819        (transcoder, server)
820    }
821
822    #[actix_web::test]
823    async fn loads_descriptors_from_live_grpc_reflection() {
824        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
825        let address = listener.local_addr().unwrap();
826        let reflection = tonic_reflection::server::Builder::configure()
827            .register_encoded_file_descriptor_set(include_bytes!(concat!(
828                env!("OUT_DIR"),
829                "/gateway.bin"
830            )))
831            .build_v1()
832            .unwrap();
833        let server = tokio::spawn(async move {
834            Server::builder()
835                .add_service(reflection)
836                .add_service(GreeterServer::new(GreeterService))
837                .serve_with_incoming(TcpListenerStream::new(listener))
838                .await
839                .unwrap();
840        });
841        let channel = tonic::transport::Endpoint::from_shared(format!("http://{address}"))
842            .unwrap()
843            .connect()
844            .await
845            .unwrap();
846        let transcoder = TranscoderBuilder::from_reflection(channel)
847            .await
848            .unwrap()
849            .add_binding(HttpBinding::new(
850                HttpVerb::Get,
851                "/v1/reflected/{id}",
852                "rust_zero.gateway_test.Greeter.Get",
853            ))
854            .build()
855            .unwrap();
856        let app = test::init_service(
857            App::new()
858                .app_data(web::Data::new(transcoder))
859                .default_service(web::to(transcode)),
860        )
861        .await;
862        let response = test::call_service(
863            &app,
864            test::TestRequest::get()
865                .uri("/v1/reflected/11?view=reflection")
866                .to_request(),
867        )
868        .await;
869        assert_eq!(response.status(), StatusCode::OK);
870        server.abort();
871    }
872
873    #[actix_web::test]
874    async fn transcodes_path_query_metadata_and_protobuf_json() {
875        let (transcoder, server) = fixture().await;
876        let app = test::init_service(
877            App::new()
878                .app_data(web::Data::new(transcoder))
879                .default_service(web::to(transcode)),
880        )
881        .await;
882        let response = test::call_service(
883            &app,
884            test::TestRequest::get()
885                .uri("/v1/greeters/7?view=full")
886                .insert_header(("x-request-id", "request-1"))
887                .to_request(),
888        )
889        .await;
890        assert_eq!(response.status(), StatusCode::OK);
891        assert_eq!(response.headers().get("x-backend").unwrap(), "grpc");
892        let body: serde_json::Value = test::read_body_json(response).await;
893        assert_eq!(body, serde_json::json!({"id": 7, "message": "full"}));
894        server.abort();
895    }
896
897    #[actix_web::test]
898    async fn streams_newline_delimited_json_and_maps_grpc_statuses() {
899        let (transcoder, server) = fixture().await;
900        let app = test::init_service(
901            App::new()
902                .app_data(web::Data::new(transcoder))
903                .default_service(web::to(transcode)),
904        )
905        .await;
906        let response = test::call_service(
907            &app,
908            test::TestRequest::get()
909                .uri("/v1/greeters/9/watch")
910                .to_request(),
911        )
912        .await;
913        assert_eq!(response.status(), StatusCode::OK);
914        assert_eq!(
915            test::read_body(response).await,
916            "{\"id\":9,\"message\":\"one\"}\n{\"id\":9,\"message\":\"two\"}\n"
917        );
918
919        let response = test::call_service(
920            &app,
921            test::TestRequest::get().uri("/v1/missing/9").to_request(),
922        )
923        .await;
924        assert_eq!(response.status(), StatusCode::NOT_FOUND);
925        let body: serde_json::Value = test::read_body_json(response).await;
926        assert_eq!(body["message"], "missing greeter");
927        server.abort();
928    }
929}