elixir-grpc / grpc

An Elixir implementation of gRPC
https://hex.pm/packages/grpc
Apache License 2.0
1.39k stars 212 forks source link

Unify `return_headers` option #291

Open beligante opened 1 year ago

beligante commented 1 year ago

Is your feature request related to a problem? Please describe. The library offers the option to receive the headers from the http request. Essentially when you pass the option - in the right place - return_headers: true the library will return the headers and the trailers (those are the headers returned at the end of the request)

So, for a unary rpc you can receive the headers using:

iex(22)> alias Routeguide.RouteGuide.Stub
iex(23)> alias Routeguide.Point

iex(25)> opts = [interceptors: [GRPC.Client.Interceptors.Logger]]
iex(26)> {:ok, channel} = GRPC.Stub.connect("localhost:10000", opts)

iex(28)> Stub.get_feature(channel, Point.new(latitude: 409_146_138, longitude: -746_188_906), return_headers: true) |> IO.inspect()
{:ok,
 <name: "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", location: <latitude: 409146138, longitude: -746188906>>,
 %{
   headers: %{
     "content-type" => "application/grpc+proto",
     "date" => "Tue, 10 Jan 2023 12:28:19 GMT",
     "server" => "Cowboy"
   },
   trailers: %{"grpc-message" => "", "grpc-status" => "0"}
 }}

And in the case of a bidirectional stream we need to pass that option in the GRPC.Stub.recv/2

iex(39)> stream = channel |> Routeguide.RouteGuide.Stub.route_chat()
iex(40)> point = Routeguide.Point.new(latitude: 0, longitude: 1)
iex(41)> note = Routeguide.RouteNote.new(location: point, message: "message")
iex(42)> GRPC.Stub.send_request(stream, note, end_stream: true)
iex(43)> {:ok, ex_stream, %{headers: headers}} = GRPC.Stub.recv(stream, return_headers: true)
{:ok, #Function<61.127921642/2 in Stream.unfold/2>,
 %{
   headers: %{
     "content-type" => "application/grpc+proto",
     "date" => "Tue, 10 Jan 2023 12:37:57 GMT",
     "server" => "Cowboy"
   }
 }}
iex(44)> Enum.to_list(ex_stream) |> IO.inspect
[
  ok: %Routeguide.RouteNote{
    __unknown_fields__: [],
    location: <latitude: 0, longitude: 1>,
    message: "message"
  },
  ok: %Routeguide.RouteNote{
    __unknown_fields__: [],
    location: <latitude: 0, longitude: 1>,
    message: "message"
  },
  ok: %Routeguide.RouteNote{
    __unknown_fields__: [],
    location: <latitude: 0, longitude: 1>,
    message: "message"
  },
  trailers: %{"grpc-message" => "", "grpc-status" => "0"}
]

Notice the following for the above:

  1. I had to pass the option in another function ( recv/2 )
  2. The headers were returned in a three position tuple and the trailers were returned inside the Elixir.Stream

Describe the solution you'd like

For this issue there are two problems that I would like to solve.

1

I would like to unify the place where we pass that option and my suggestion here is to pass that when we're starting the request. In this case, unary call stay unchanged. But for bidi-streams we could pass the option like this:

stream = Routeguide.RouteGuide.Stub.route_chat(channel, return_headers: true)
# no longer passing the option in recv/2
{:ok, ex_stream, %{headers: headers}} = GRPC.Stub.recv(stream)

2

For bidi-streams we could return the headers as a member of the Elixir.Stream. In this case, when we invoke recv/2 we would return a two position tuple. Like:

stream = Routeguide.RouteGuide.Stub.route_chat(channel, return_headers: true)
# no longer receiving a three position tuple in recv/2
{:ok, ex_stream} = GRPC.Stub.recv(stream)
iex(17)> [ex_stream |> Enum.to_list() |> IO.inspect()
[
  headers: %{
    "content-type" => "application/grpc+proto",
    "date" => "Tue, 10 Jan 2023 12:49:47 GMT",
    "server" => "Cowboy"
  },
  ok: %Routeguide.RouteNote{
    __unknown_fields__: [],
    location: <latitude: 0, longitude: 1>,
    message: "message"
  },
  trailers: %{"grpc-message" => "", "grpc-status" => "0"}
]

Additional context This will be a breaking change and also will require changes on both (gun and min) adapters