Repository: elixir-grpc/grpc
Pull request #171 (2020-08-19 12:47:00)

Provide a mechanism to inject a custom status handler
## Problem
In order to support Traefik we need to listen in the same port the h1 health checks, more info here https://github.com/containous/traefik/issues/6211

## Expected outcome
Add a way to inject a custom status handler, so we can listen for h1 requests in the same port using the desired handler.

Let me know if it works for you in order to update the doc as well or if you have another solution.

The configuration could be:
```elixir
config :grpc,
       :status_handler,
       {"/status", Phoenix.Endpoint.Cowboy2Handler, {My.Endpoint, []}}
```

carrascoacd: @tony612 could you review it when you have time, please?
polvalente: @carrascoacd is this still relevant?

Patch:
diff --git a/lib/grpc/adapter/cowboy.ex b/lib/grpc/adapter/cowboy.ex
index 31bc099..13a6411 100644
--- a/lib/grpc/adapter/cowboy.ex
+++ b/lib/grpc/adapter/cowboy.ex
@@ -153,11 +153,22 @@ defmodule GRPC.Adapter.Cowboy do
   end
 
   defp cowboy_start_args(endpoint, servers, port, opts) do
-    dispatch =
-      :cowboy_router.compile([
-        {:_, [{:_, GRPC.Adapter.Cowboy.Handler, {endpoint, servers, Enum.into(opts, %{})}}]}
-      ])
+    # Custom handler to be able to listen in the same port, more info:
+    # https://github.com/containous/traefik/issues/6211
+    {adapter_opts, opts} = Keyword.pop(opts, :adapter_opts, [])
+    status_handler = Keyword.get(adapter_opts, :status_handler)
 
+    handlers =
+      if status_handler do
+        [
+          status_handler,
+          {:_, GRPC.Adapter.Cowboy.Handler, {endpoint, servers, Enum.into(opts, %{})}}
+        ]
+      else
+        [{:_, GRPC.Adapter.Cowboy.Handler, {endpoint, servers, Enum.into(opts, %{})}}]
+      end
+
+    dispatch = :cowboy_router.compile([{:_, handlers}])
     idle_timeout = Keyword.get(opts, :idle_timeout, :infinity)
     num_acceptors = Keyword.get(opts, :num_acceptors, @default_num_acceptors)
     max_connections = Keyword.get(opts, :max_connections, @default_max_connections)
diff --git a/lib/grpc/server.ex b/lib/grpc/server.ex
index cb06137..fd6e5d3 100644
--- a/lib/grpc/server.ex
+++ b/lib/grpc/server.ex
@@ -233,6 +233,9 @@ defmodule GRPC.Server do
   #   * `:cred` - a credential created by functions of `GRPC.Credential`,
   #               an insecure server will be created without this option
   #   * `:adapter` - use a custom server adapter instead of default `GRPC.Adapter.Cowboy`
+  #   * `:adapter_opts` - configuration for the specified adapter.
+  #     * `:status_handler` - adds a status handler that could be listening on HTTP/1, if necessary.
+  #                           It should follow the format defined by cowboy_router:compile/3
   @doc false
   @spec start(servers_list, non_neg_integer, Keyword.t()) :: {atom, any, non_neg_integer}
   def start(servers, port, opts \\ []) do


Test patch:
diff --git a/test/grpc/integration/server_test.exs b/test/grpc/integration/server_test.exs
index c80f814..e245571 100644
--- a/test/grpc/integration/server_test.exs
+++ b/test/grpc/integration/server_test.exs
@@ -98,6 +98,12 @@ defmodule GRPC.Integration.ServerTest do
     end
   end
 
+  defmodule HTTP1Server do
+    def init(req, state) do
+      {:ok, :cowboy_req.reply(200, %{}, "OK", req), state}
+    end
+  end
+
   test "multiple servers works" do
     run_server([FeatureServer, HelloServer], fn port ->
       {:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
@@ -111,6 +117,27 @@ defmodule GRPC.Integration.ServerTest do
     end)
   end
 
+  test "HTTP/1 status handler can be started along a gRPC server" do
+    status_handler = {"/status", HTTP1Server, []}
+
+    run_server(
+      [HelloServer],
+      fn port ->
+        {:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
+        req = Helloworld.HelloRequest.new(name: "Elixir")
+        {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req)
+        assert reply.message == "Hello, Elixir"
+
+        {:ok, conn_pid} = :gun.open('localhost', port)
+        stream_ref = :gun.get(conn_pid, "/status")
+
+        assert_receive {:gun_response, ^conn_pid, ^stream_ref, :nofin, 200, _headers}
+      end,
+      0,
+      adapter_opts: [status_handler: status_handler]
+    )
+  end
+
   test "returns appropriate error for unary requests" do
     run_server([HelloErrorServer], fn port ->
       {:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
diff --git a/test/support/integration_test_case.ex b/test/support/integration_test_case.ex
index df1daaa..24467ca 100644
--- a/test/support/integration_test_case.ex
+++ b/test/support/integration_test_case.ex
@@ -9,8 +9,8 @@ defmodule GRPC.Integration.TestCase do
     end
   end
 
-  def run_server(servers, func, port \\ 0) do
-    {:ok, _pid, port} = GRPC.Server.start(servers, port)
+  def run_server(servers, func, port \\ 0, opts \\ []) do
+    {:ok, _pid, port} = GRPC.Server.start(servers, port, opts)
 
     try do
       func.(port)