From de8ea03bbdd831bae532f06ea95527ea2e0d98a3 Mon Sep 17 00:00:00 2001 From: Adriano Santos Date: Wed, 8 Apr 2026 14:38:04 -0300 Subject: [PATCH 1/3] chore: added notes regarding the separation of client and server packages. --- README.md | 7 ++++++- grpc_server/README.md | 9 ++++++--- mix.lock | 1 + 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b9845e11..c3c26d05 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,15 @@ The package can be installed as: def deps do [ {:grpc, "~> 0.11"}, + {:grpc_server, "~> 0.11"}, {:protobuf, "~> 0.14"}, # optional for import wellknown google types {:grpc_reflection, "~> 0.2"} # optional enable grpc reflection ] end ``` +>**_Note (since v1.0.0-rc.1)_**: To use the server, you must add the :grpc_server dependency to your mix.exs instead of the older :grpc package. + ## Protobuf Code Generation Use `protoc` with [protobuf elixir plugin](https://github.com/elixir-protobuf/protobuf) or using [protobuf_generate](https://hexdocs.pm/protobuf_generate/readme.html) hex package to generate the necessary files. @@ -91,7 +94,7 @@ protoc --elixir_out=plugins=grpc:./lib -I./priv/protos helloworld.proto All RPC calls must be implemented using the stream-based API, even for unary requests. ->__NOTE__: The old API was deprecated based on `GRPC.Server.send_reply/2` and direct `struct` returns was deprecated as of version `0.10.x`. +>**_Note_**: The old API was deprecated based on `GRPC.Server.send_reply/2` and direct `struct` returns was deprecated as of version `0.10.x`. ### Unary RPC using Stream API @@ -283,6 +286,8 @@ end This section demonstrates how to establish client connections and perform RPC calls using the Elixir gRPC client. +>**_Note (since v1.0.0-rc.1)_**: To use the client, you must explicitly add the :grpc dependency to your mix.exs. + --- ## Basic Connection and RPC diff --git a/grpc_server/README.md b/grpc_server/README.md index 4ad3500f..b88fbbb7 100644 --- a/grpc_server/README.md +++ b/grpc_server/README.md @@ -42,6 +42,8 @@ def deps do end ``` +>**_Note (since v1.0.0-rc.1)_**: To use the server, you must add the :grpc_server dependency to your mix.exs instead of the older :grpc package. + ## Protobuf Code Generation Use `protoc` with [protobuf elixir plugin](https://github.com/elixir-protobuf/protobuf) or using [protobuf_generate](https://hexdocs.pm/protobuf_generate/readme.html) hex package to generate the necessary files. @@ -81,7 +83,7 @@ protoc --elixir_out=plugins=grpc:./lib -I./priv/protos helloworld.proto All RPC calls must be implemented using the stream-based API, even for unary requests. ->__NOTE__: The old API was deprecated based on `GRPC.Server.send_reply/2` and direct `struct` returns was deprecated as of version `0.10.x`. +>**_Note_**: The old API was deprecated based on `GRPC.Server.send_reply/2` and direct `struct` returns was deprecated as of version `0.10.x`. ### Unary RPC using Stream API @@ -236,7 +238,7 @@ By normalizing all possible outcomes, `GRPC.Stream` ensures fault-tolerant, exce This unified model allows developers to build composable and reliable streaming pipelines that gracefully recover from both domain and runtime errors. ->_NOTE_: In the example above, we could use `map_error/2` instead of `map/2` to handle error cases explicitly. However, since the function also performs a transformation on successful values, `map/2` remains appropriate and useful in this context. +>**_Note_**: In the example above, we could use `map_error/2` instead of `map/2` to handle error cases explicitly. However, since the function also performs a transformation on successful values, `map/2` remains appropriate and useful in this context. --- @@ -287,6 +289,7 @@ A TLS configuration can be defined by assigning a `%GRPC.Credential{}` struct, t This section demonstrates how to establish client connections and perform RPC calls using the Elixir gRPC client. +>**_Note (since v1.0.0-rc.1)_**: To use the client, you must explicitly add the :grpc dependency to your mix.exs. --- ## Basic Connection and RPC @@ -362,7 +365,7 @@ iex> {:ok, reply} = channel |> Orders.OrderService.Stub.get_order(request) iex> {:ok, channel} = GRPC.Stub.connect("unix:/tmp/my.sock") ``` ->__NOTE__: When using `DNS` or `xDS` targets, the connection layer periodically refreshes endpoints. +>**_Note_**: When using `DNS` or `xDS` targets, the connection layer periodically refreshes endpoints. --- ## Compression and Metadata diff --git a/mix.lock b/mix.lock index 427f40b3..ffa77b64 100644 --- a/mix.lock +++ b/mix.lock @@ -4,6 +4,7 @@ "flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"}, "gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"}, "googleapis": {:hex, :googleapis, "0.1.0", "13770f3f75f5b863fb9acf41633c7bc71bad788f3f553b66481a096d083ee20e", [:mix], [{:protobuf, "~> 0.12", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1989a7244fd17d3eb5f3de311a022b656c3736b39740db46506157c4604bd212"}, + "grpc_core": {:hex, :grpc_core, "1.0.0-rc.1", "d82957bca32937bb52df06596cca7550783acc139a06b70202a982ef8b59490e", [:mix], [{:googleapis, "~> 0.1.0", [hex: :googleapis, repo: "hexpm", optional: false]}, {:jason, ">= 0.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.14", [hex: :protobuf, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c76233ea374421da562b5b022c22614e81f9cf862da93543cff93c37c085f136"}, "gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, From a813e2df4400312bfc5c4e5cabbb7a9065b2e77c Mon Sep 17 00:00:00 2001 From: VovaGula Date: Fri, 24 Jul 2026 17:57:45 +0200 Subject: [PATCH 2/3] fix(mint): handle stream-level errors in process_response instead of crashing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mint.HTTP2.stream/2 returns {:error, request_ref, %Mint.HTTPError{}} entries for requests that were in flight when the server sent GOAWAY (reason: :unprocessed). process_response/2 had no clause for error tuples, so the whole connection process crashed with FunctionClauseError — bypassing both the reconnection logic and the {:elixir_grpc, :connection_down, pid} parent notification, and killing or hanging every caller with an in-flight request. Deliver the error to the request's stream response process and drop the request, mirroring the connection-close path. --- .../mint/connection_process/connection_process.ex | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/grpc/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/grpc/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 973ad574..81b74648 100644 --- a/grpc/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/grpc/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -278,6 +278,20 @@ if Code.ensure_loaded?(Mint.HTTP) do state end + # A stream-level error, e.g. Mint.HTTPError{reason: :unprocessed} for + # requests that were in flight when the server sent GOAWAY. Deliver the + # error to the caller and drop the request instead of crashing the whole + # connection process with a FunctionClauseError (which also skips the + # retry logic and the :connection_down notification). + defp process_response({:error, request_ref, error}, state) do + pid = State.stream_response_pid(state, request_ref) + :ok = StreamResponseProcess.consume(pid, :error, error) + :ok = StreamResponseProcess.done(pid) + + {_ref, new_state} = State.pop_ref(state, request_ref) + new_state + end + defp process_response({:done, request_ref}, state) do :ok = state From 43f3e5334fe01b4f01fb98f91412433119eb6fbb Mon Sep 17 00:00:00 2001 From: VovaGula Date: Fri, 24 Jul 2026 19:00:51 +0200 Subject: [PATCH 3/3] test(mint): cover stream-level errors delivered to the response process MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Injects a raw RST_STREAM(REFUSED_STREAM) frame for an in-flight request as if it arrived on the socket — the same {:error, ref, %Mint.HTTPError{}} response entry shape that GOAWAY produces for raced requests. Runs against a bare TCP server that only completes the HTTP/2 preface and then stays silent, so no real server frames race the injected one. Without the fix the test fails with the original FunctionClauseError in process_response/2. --- .../adapters/mint/connection_process_test.exs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/grpc/test/grpc/adapters/mint/connection_process_test.exs b/grpc/test/grpc/adapters/mint/connection_process_test.exs index 3781b781..389bf573 100644 --- a/grpc/test/grpc/adapters/mint/connection_process_test.exs +++ b/grpc/test/grpc/adapters/mint/connection_process_test.exs @@ -213,6 +213,41 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do end end + describe "handle_info/2 - stream errors" do + setup :quiet_connection + setup :valid_stream_request + setup :valid_stream_response + + test "delivers per-stream errors to the stream response process instead of crashing", + %{request_ref: _request_ref, stream_response_pid: response_pid, state: state} do + # A server can fail a single stream while the connection stays healthy — + # e.g. RST_STREAM(REFUSED_STREAM), or GOAWAY racing requests that were + # already in flight. Mint.HTTP2.stream/2 then returns the raced request + # as an {:error, ref, %Mint.HTTPError{}} entry in the responses list. + # Inject a raw RST_STREAM frame for our in-flight request as if it + # arrived on the socket: length 4, type 0x3 (RST_STREAM), flags 0, + # the request's stream id; payload: error code 0x7 (REFUSED_STREAM). + stream_id = state.conn.next_stream_id - 2 + rst_stream_frame = <<4::24, 0x03, 0, stream_id::32, 0x07::32>> + message = {:tcp, state.conn.socket, rst_stream_frame} + + assert {:noreply, new_state} = ConnectionProcess.handle_info(message, state) + + # the request is dropped, the connection process and connection survive + assert %{} == new_state.requests + assert Mint.HTTP.open?(new_state.conn) + + # the caller-facing stream response process got the error and was closed + response_state = :sys.get_state(response_pid) + assert true == response_state.done + + assert [{:error, %Mint.HTTPError{reason: {:server_closed_request, :refused_stream}}}] = + :queue.to_list(response_state.responses) + + refute_receive {:elixir_grpc, :connection_down, _pid} + end + end + describe "handle_continue/2 - :process_stream_queue" do setup :valid_connection setup :valid_stream_request @@ -522,6 +557,53 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do end end + # Like valid_connection/2, but against a bare TCP server that only completes + # the HTTP/2 preface (server SETTINGS + ack) and then stays silent — so no + # real server frames race the ones injected by the test. + defp quiet_connection(_ctx) do + {:ok, listen_socket} = :gen_tcp.listen(0, [:binary, active: false, reuseaddr: true]) + {:ok, port} = :inet.port(listen_socket) + test_pid = self() + + spawn_link(fn -> + {:ok, socket} = :gen_tcp.accept(listen_socket) + :ok = :gen_tcp.send(socket, <<0::24, 0x04, 0, 0::32>>) + :ok = :gen_tcp.send(socket, <<0::24, 0x04, 0x01, 0::32>>) + + receive do + :close -> :gen_tcp.close(socket) + end + end) + + on_exit(fn -> :gen_tcp.close(listen_socket) end) + + {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port, protocols: [:http2]) + + # The state snapshot must only be taken once the connection has processed + # the server preface: HTTP/2 requires SETTINGS to be the first server + # frame, so a snapshot taken earlier would treat the frame injected by the + # test as a protocol error. + wait_until_open(pid) + + state = :sys.get_state(pid) + version = Application.spec(:grpc) |> Keyword.get(:vsn) + + headers = [ + {"content-type", "application/grpc"}, + {"user-agent", "grpc-elixir/#{version}"}, + {"te", "trailers"} + ] + + _ = test_pid + + %{ + process_pid: pid, + state: state, + port: port, + request: {"POST", "/routeguide.RouteGuide/RecordRoute", headers} + } + end + defp valid_connection(%{port: port}, opts \\ []) do {:ok, pid} = ConnectionProcess.start_link( @@ -570,4 +652,15 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do end defp valid_connection_with_retry(ctx), do: valid_connection(ctx, retry: 3) + + defp wait_until_open(pid, attempts_left \\ 100) do + if attempts_left == 0, do: raise("connection did not finish the HTTP/2 handshake") + + if :sys.get_state(pid).conn.state == :open do + :ok + else + Process.sleep(10) + wait_until_open(pid, attempts_left - 1) + end + end end