Skip to content

Mint adapter never checks the HTTP status, continuing as if the call succeeded #596

Description

@arrangabriel

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:
    []
    ...

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions