The Mint adapter does not check the HTTP status of a response, so the body is processed as if everything is fine. Whereas the Gun adapter returns an RPCError if status != 200.
Say a service is behind a load-balancer, that at some point returns 502.
The (probably HTML) body is handed to StreamResponseProcess.consume/3 like any other data frame,
but GRPC.Message.get_message/2 cannot read amessage from it,
so nothing is enqueued into responses_queue.
This leads do_receive_data/3 in the adapter module to fail, as it assumes an :ok entry is present in the
responses list if check_for_error/1 finds no error:
case check_for_error(responses) do
:ok ->
data = Keyword.fetch!(responses, :ok)
(...)
def check_for_error(responses) do
error = Keyword.get(responses, :error)
if error, do: {:error, error}, else: :ok
end
responses is [], so Keyword.fetch!/2 raises out of GRPC.Stub into the calling process.
Reproduction
A unary call against a Bandit plug that answers with a plain HTTP 502, first through the Gun adapter and then through the Mint adapter.
Mix.install([
{:grpc, github: "elixir-grpc/grpc", sparse: "grpc"},
{:mint, "~> 1.10"},
{:gun, "~> 2.4"},
{:bandit, "~> 1.12"}
])
defmodule Message do
use Protobuf, syntax: :proto3
field(:message, 1, type: :string)
end
defmodule Ping.Service do
use GRPC.Service, name: "ping.Ping"
rpc(:Ping, Message, Message)
end
defmodule Ping.Stub do
use GRPC.Stub, service: Ping.Service
end
defmodule Proxy do
@response "<html><head><title>502 Bad Gateway</title></head></html>"
def init(opts), do: opts
def call(conn, _opts), do: Plug.Conn.send_resp(conn, 502, @response)
end
{:ok, _proxy} = Bandit.start_link(plug: Proxy, port: 50_123, scheme: :http)
request = struct(Message, message: "ping")
# The Gun adapter checks the HTTP status and returns an error.
{:ok, gun} = GRPC.Stub.connect("localhost:50123", adapter: GRPC.Client.Adapters.Gun)
IO.puts("ping through gun")
IO.inspect(Ping.Stub.ping(gun, request, timeout: 1_000))
# The Mint adapter raises.
{:ok, mint} = GRPC.Stub.connect("localhost:50123", adapter: GRPC.Client.Adapters.Mint)
IO.puts("ping through mint")
Ping.Stub.ping(mint, request, timeout: 1_000)
Gun reports the status, Mint raises. The exception is logged before it is re-raised, and neither the log nor the raise names mint.ex — GRPC.Telemetry.client_span/3 re-raises with :erlang.error/1, which discards the original stacktrace:
ping through gun
{:error,
%GRPC.RPCError{
status: 13,
message: "status got is 502 instead of 200",
details: nil
}}
ping through mint
[error] ** (KeyError) key :ok not found in:
[]
(grpc_core 1.0.5) lib/grpc/telemetry.ex:125: anonymous fn/2 in GRPC.Telemetry.client_span/3
(telemetry 1.4.2) telemetry.erl:359: :telemetry.span/3
(grpc_core 1.0.5) lib/grpc/telemetry.ex:118: GRPC.Telemetry.client_span/3
repro.exs:40: (file)
** (KeyError) key :ok not found in:
[]
...
The Mint adapter does not check the HTTP status of a response, so the body is processed as if everything is fine. Whereas the Gun adapter returns an
RPCErrorif status != 200.Say a service is behind a load-balancer, that at some point returns 502.
The (probably HTML) body is handed to
StreamResponseProcess.consume/3like any other data frame,but
GRPC.Message.get_message/2cannot read amessage from it,so nothing is enqueued into
responses_queue.This leads
do_receive_data/3in the adapter module to fail, as it assumes an:okentry is present in theresponses list if
check_for_error/1finds no error:responsesis[], soKeyword.fetch!/2raises out ofGRPC.Stubinto the calling process.Reproduction
A unary call against a Bandit plug that answers with a plain HTTP 502, first through the Gun adapter and then through the Mint adapter.
Gun reports the status, Mint raises. The exception is logged before it is re-raised, and neither the log nor the raise names
mint.ex—GRPC.Telemetry.client_span/3re-raises with:erlang.error/1, which discards the original stacktrace: