gRPC-JSON transcoder

This is a filter which allows a RESTful JSON API client to send requests to Envoy over HTTP and get proxied to a gRPC service. The HTTP mapping for the gRPC service has to be defined by custom options.

JSON mapping

The protobuf to JSON mapping is defined here. For gRPC stream request parameters, Envoy expects an array of messages, and it returns an array of messages for stream response parameters.

How to generate proto descriptor set

Envoy has to know the proto descriptor of your gRPC service in order to do the transcoding.

To generate a protobuf descriptor set for the gRPC service, you’ll also need to clone the googleapis repository from GitHub before running protoc, as you’ll need annotations.proto in your include path, to define the HTTP mapping.

  1. $ git clone https://github.com/googleapis/googleapis
  2. $ GOOGLEAPIS_DIR=<your-local-googleapis-folder>

Then run protoc to generate the descriptor set. For example using the test bookstore.proto provided in the Envoy repository:

  1. $ protoc -I$(GOOGLEAPIS_DIR) -I. --include_imports --include_source_info \
  2. --descriptor_set_out=proto.pb test/proto/bookstore.proto

If you have more than one proto source files, you can pass all of them in one command.

Route configs for transcoded requests

The route configs to be used with the gRPC-JSON transcoder should be identical to the gRPC route. The requests processed by the transcoder filter will have /<package>.<service>/<method> path and POST method. The route configs for those requests should match on /<package>.<service>/<method>, not the incoming request path. This allows the routes to be used for both gRPC requests and gRPC-JSON transcoded requests.

For example, with the following proto example, the router will process /helloworld.Greeter/SayHello as the path, so the route config prefix /say won’t match requests to SayHello. If you want to match the incoming request path, set match_incoming_request_route to true.

  1. syntax = "proto3";
  2. package helloworld;
  3. import "google/api/annotations.proto";
  4. // The greeting service definition.
  5. service Greeter {
  6. // Sends a greeting
  7. rpc SayHello(HelloRequest) returns (HelloReply) {
  8. option (google.api.http) = {
  9. get: "/say"
  10. };
  11. }
  12. }
  13. // The request message containing the user's name.
  14. message HelloRequest {
  15. string name = 1;
  16. }
  17. // The response message containing the greetings
  18. message HelloReply {
  19. string message = 1;
  20. }

Assuming you have checked out the google APIs as described above, and have saved the proto file as protos/helloworld.proto you can build it with:

  1. $ protoc -I$(GOOGLEAPIS_DIR) -I. --include_imports --include_source_info \
  2. --descriptor_set_out=protos/helloworld.pb protos/helloworld.proto

Sending arbitrary content

By default, when transcoding occurs, gRPC-JSON encodes the message output of a gRPC service method into JSON and sets the HTTP response Content-Type header to application/json. To send arbitrary content, a gRPC service method can use google.api.HttpBody as its output message type. The implementation needs to set content_type (which sets the value of the HTTP response Content-Type header) and data (which sets the HTTP response body) accordingly. Multiple google.api.HttpBody can be send by the gRPC server in the server streaming case. In this case, HTTP response header Content-Type will use the content-type from the first google.api.HttpBody.

Headers

gRPC-JSON forwards the following headers to the gRPC server:

  • x-envoy-original-path, containing the value of the original path of HTTP request

  • x-envoy-original-method, containing the value of the original method of HTTP request

Sample Envoy configuration

Here’s a sample Envoy configuration that proxies to a gRPC server running on localhost:50051. Port 51051 proxies gRPC requests and uses the gRPC-JSON transcoder filter to provide the RESTful JSON mapping. I.e., you can make either gRPC or RESTful JSON requests to localhost:51051.

  1. admin:
  2. address:
  3. socket_address: {address: 0.0.0.0, port_value: 9901}
  4. static_resources:
  5. listeners:
  6. - name: listener1
  7. address:
  8. socket_address: {address: 0.0.0.0, port_value: 51051}
  9. filter_chains:
  10. - filters:
  11. - name: envoy.filters.network.http_connection_manager
  12. typed_config:
  13. "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
  14. stat_prefix: grpc_json
  15. codec_type: AUTO
  16. route_config:
  17. name: local_route
  18. virtual_hosts:
  19. - name: local_service
  20. domains: ["*"]
  21. routes:
  22. # NOTE: by default, matching happens based on the gRPC route, and not on the incoming request path.
  23. # Reference: https://envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter#route-configs-for-transcoded-requests
  24. - match: {prefix: "/helloworld.Greeter"}
  25. route: {cluster: grpc, timeout: 60s}
  26. http_filters:
  27. - name: envoy.filters.http.grpc_json_transcoder
  28. typed_config:
  29. "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
  30. proto_descriptor: "protos/helloworld.pb"
  31. services: ["helloworld.Greeter"]
  32. print_options:
  33. add_whitespace: true
  34. always_print_primitive_fields: true
  35. always_print_enums_as_ints: false
  36. preserve_proto_field_names: false
  37. - name: envoy.filters.http.router
  38. clusters:
  39. - name: grpc
  40. type: LOGICAL_DNS
  41. lb_policy: ROUND_ROBIN
  42. dns_lookup_family: V4_ONLY
  43. typed_extension_protocol_options:
  44. envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
  45. "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
  46. explicit_http_config:
  47. http2_protocol_options: {}
  48. load_assignment:
  49. cluster_name: grpc
  50. endpoints:
  51. - lb_endpoints:
  52. - endpoint:
  53. address:
  54. socket_address:
  55. # WARNING: "docker.for.mac.localhost" has been deprecated from Docker v18.03.0.
  56. # If you're running an older version of Docker, please use "docker.for.mac.localhost" instead.
  57. # Reference: https://docs.docker.com/docker-for-mac/release-notes/#docker-community-edition-18030-ce-mac59-2018-03-26
  58. address: host.docker.internal
  59. port_value: 50051