Transcoding
The goal of transcoding is to allow HTTP/JSON calls to be automatically converted into gRPC protobuf calls without external gateways.
Setup
app_root = Path.join(__DIR__, "..")
Mix.install(
[
{:grpc, path: app_root, env: :dev}
],
config_path: Path.join(app_root, "config/config.exs"),
lockfile: Path.join(app_root, "mix.lock")
)
Protobuf Service and Messages
defmodule Helloworld.HelloRequest do
@moduledoc false
use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
field :name, 1, type: :string
end
defmodule Helloworld.HelloRequestFrom do
@moduledoc false
use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
field :name, 1, type: :string
field :from, 2, type: :string
end
defmodule Helloworld.HelloReply do
@moduledoc false
use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
field :message, 1, type: :string
field :today, 2, type: Google.Protobuf.Timestamp
end
defmodule Helloworld.Greeter.Service do
@moduledoc false
use GRPC.Service, name: "helloworld.Greeter", protoc_gen_elixir_version: "0.14.1"
rpc(:SayHello, Helloworld.HelloRequest, Helloworld.HelloReply, %{
http: %{
type: Google.Api.PbExtension,
value: %Google.Api.HttpRule{
selector: "",
body: "",
additional_bindings: [],
response_body: "",
pattern: {:get, "/v1/greeter/{name}"},
__unknown_fields__: []
}
}
})
rpc(:SayHelloFrom, Helloworld.HelloRequestFrom, Helloworld.HelloReply, %{
http: %{
type: Google.Api.PbExtension,
value: %Google.Api.HttpRule{
selector: "",
body: "*",
additional_bindings: [],
response_body: "",
pattern: {:post, "/v1/greeter"},
__unknown_fields__: []
}
}
})
end
In a real-world application, this would be generated from your project’s .proto files. You would have to annotate your protobuf in the following way:
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
get: "/v1/greeter/{name}"
};
}
rpc SayHelloFrom (HelloRequestFrom) returns (HelloReply) {
option (google.api.http) = {
post: "/v1/greeter"
body: "*"
};
}
}
The compilation itself would be something like:
mix protobuf.generate --include-path=priv/proto --include-path=deps/googleapis --generate-descriptors=true --output-path=./lib --plugins=ProtobufGenerate.Plugins.GRPCWithOptions google/api/annotations.proto google/api/http.proto helloworld.proto
Enable transcoding on the Elixir side.
defmodule Helloworld.Greeter.Server do
use GRPC.Server,
service: Helloworld.Greeter.Service,
http_transcode: true
alias GRPC.Stream, as: GRPCStream
alias Helloworld.HelloRequest
alias Helloworld.HelloReply
def say_hello(request, stream) do
GRPCStream.unary(request, materializer: stream)
|> GRPCStream.map(fn
%HelloRequest{} = reply ->
%HelloReply{
message: "Hello #{request.name}",
today: today()
}
{:error, reason} ->
{:error, GRPC.RPCError.exception(message: "[Error] #{inspect(reason)}")}
end)
|> GRPCStream.run()
end
def say_hello_from(request, _stream) do
GRPCStream.unary(request, materializer: stream)
|> GRPCStream.map(fn
%HelloFromRequest{} = reply ->
%HelloReply{
message: "Hello #{request.name}. From #{request.from}",
today: today()
}
_ ->
GRPC.RPCError.exception(message: "[Error] something bad happened")
end)
|> GRPCStream.run()
end
defp today() do
nanos_epoch = System.system_time() |> System.convert_time_unit(:native, :nanosecond)
seconds = div(nanos_epoch, 1_000_000_000)
nanos = nanos_epoch - seconds * 1_000_000_000
%Google.Protobuf.Timestamp{seconds: seconds, nanos: nanos}
end
end
Endpoint + Supervisor
defmodule TranscodeEndpoint do
use GRPC.Endpoint
intercept(GRPC.Server.Interceptors.Logger)
run(Helloworld.Greeter.Server)
end
{:ok, _pid} =
GRPC.Server.Supervisor.start_link(
endpoint: TranscodeEndpoint,
port: 50054,
start_server: true
)
IO.puts("Transcoded gRPC Server running on :50054")
This automatically activates HTTP endpoints based on the annotations.
Testing with curl
# Say hello
$ curl -H 'accept: application/json' http://localhost:50054/v1/greeter/test
# Say hello from
$ curl -XPOST -H 'Content-type: application/json' -d '{"name": "test", "from": "anon"}' http://localhost:50054/v1/greeter
Important notes
| Feature | Status |
|---|---|
| CORS | Not enabled by default. See CORS section. |
| Server Streaming | Supported. |
| Query params → fields | Supported. See note below. |
>Note: gRPC Transcode HttpRule https://docs.cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api#google.api.HttpRule