Repository: elixir-grpc/grpc
Pull request #407 (2025-02-28 18:14:26)

if an erlang error escapes it causes ReportException to fail and hide…
… the real error


Patch:
diff --git a/lib/grpc/server/adapters/report_exception.ex b/lib/grpc/server/adapters/report_exception.ex
index 8403514..47abe6e 100644
--- a/lib/grpc/server/adapters/report_exception.ex
+++ b/lib/grpc/server/adapters/report_exception.ex
@@ -5,10 +5,24 @@ defmodule GRPC.Server.Adapters.ReportException do
 
   defexception [:kind, :reason, :stack, :adapter_extra]
 
-  def new(adapter_extra, %{__exception__: _} = exception, stack \\ [], kind \\ :error) do
+  def new(adapter_extra, exception, stack \\ [], kind \\ :error)
+
+  def new(adapter_extra, %{__exception__: _} = exception, stack, kind) do
     exception(kind: kind, reason: exception, stack: stack, adapter_extra: adapter_extra)
   end
 
+  def new(adapter_extra, {erl_error, erl_stack}, _stack, kind) do
+    new(adapter_extra, Exception.normalize(:error, erl_error, erl_stack), erl_stack, kind)
+  end
+
+  def new(adapter_extra, erl_error, stack, kind) do
+    new(adapter_extra, Exception.normalize(:error, erl_error, stack), stack, kind)
+  end
+
+  def message(%__MODULE__{adapter_extra: [{:req, :ok}], kind: kind, reason: reason, stack: stack}) do
+    Exception.format_banner(kind, reason, stack)
+  end
+
   def message(%__MODULE__{adapter_extra: [{:req, req}], kind: kind, reason: reason, stack: stack}) do
     # Cowboy adapter message builder
     path = :cowboy_req.path(req)


Test patch:
diff --git a/test/grpc/server/adapters/report_exception_test.exs b/test/grpc/server/adapters/report_exception_test.exs
new file mode 100644
index 0000000..759b6f5
--- /dev/null
+++ b/test/grpc/server/adapters/report_exception_test.exs
@@ -0,0 +1,91 @@
+defmodule ExceptionServer do
+  use GenServer
+
+  # Callbacks
+
+  @impl true
+  def init(pid) do
+    {:ok, pid}
+  end
+
+  @impl true
+  def handle_cast(:case_boom, state) do
+    a = fn -> :ok end
+
+    case a.() do
+      :error -> :boom
+    end
+
+    {:noreply, state}
+  end
+
+  @impl true
+  def handle_cast(:bad_arg_boom, state) do
+    ets = :ets.new(:foo, [])
+    :ets.delete(ets)
+    :ets.insert(ets, 1)
+
+    {:noreply, state}
+  end
+
+  @impl true
+  def terminate(reason, state) do
+    send(state, {:boom, reason})
+    :ok
+  end
+end
+
+defmodule GRPC.Server.Adapters.ReportExceptionTest do
+  use ExUnit.Case, async: true
+  alias GRPC.Server.Adapters.ReportException
+
+  describe "new/3" do
+    test "with runtime error" do
+      assert %GRPC.Server.Adapters.ReportException{
+               __exception__: true,
+               adapter_extra: [req: :ok],
+               kind: :error,
+               reason: %RuntimeError{message: "hi", __exception__: true},
+               stack: []
+             } == ReportException.new([req: :ok], RuntimeError.exception("hi"))
+    end
+
+    test "with case clause error" do
+      {:ok, pid} = GenServer.start_link(ExceptionServer, self())
+
+      GenServer.cast(pid, :case_boom)
+
+      receive do
+        {:boom, {_reason, stack} = err} ->
+          assert %GRPC.Server.Adapters.ReportException{
+                   __exception__: true,
+                   adapter_extra: [req: :ok],
+                   kind: :error,
+                   reason: %CaseClauseError{term: :ok},
+                   stack: stack
+                 } == ReportException.new([{:req, :ok}], err)
+      end
+    end
+
+    test "with badarg error" do
+      {:ok, pid} = GenServer.start_link(ExceptionServer, self())
+
+      GenServer.cast(pid, :bad_arg_boom)
+
+      receive do
+        {:boom, {_reason, stack} = err} ->
+          assert %GRPC.Server.Adapters.ReportException{
+                   __exception__: true,
+                   adapter_extra: [req: :ok],
+                   kind: :error,
+                   reason: %ArgumentError{
+                     __exception__: true,
+                     message:
+                       "errors were found at the given arguments:\n\n  * 1st argument: the table identifier does not refer to an existing ETS table\n  * 2nd argument: not a tuple\n"
+                   },
+                   stack: stack
+                 } == ReportException.new([{:req, :ok}], err)
+      end
+    end
+  end
+end