Skip to content

fix(grpc): enforce types with Dialyzer and fix related violations#551

Closed
smartinio wants to merge 2 commits into
elixir-grpc:masterfrom
smartinio:smartinio/fix-connect-dialyzer
Closed

fix(grpc): enforce types with Dialyzer and fix related violations#551
smartinio wants to merge 2 commits into
elixir-grpc:masterfrom
smartinio:smartinio/fix-connect-dialyzer

Conversation

@smartinio

Copy link
Copy Markdown
Contributor

Problem

Dialyzer reports GRPC.Stub.connect/2 as returning a value outside its own spec.

connect/2 returns a GRPC.Channel struct, but GRPC.Channel.t() is stricter than the channel states built by the client connection code. Downstream projects that dialyze calls to connect/2 see that as a broken public type.

::warning file=lib/grpc/client/connection.ex,line=190,title=call::The function call finalize_connection will not succeed.
::warning file=lib/grpc/client/connection.ex,line=193,title=call::The function call finalize_connection will not succeed.
::warning file=lib/grpc/client/connection.ex,line=354,title=no_return::Function finalize_connection/2 has no local return.
::warning file=lib/grpc/client/connection.ex,line=355,title=call::The function call pick_channel will not succeed.

GRPC.Client.Stream.t() has the same issue for fields that start as nil and are filled later.

Turning Dialyzer on also catches a Gun server stream trailer bug. Embedded gRPC trailers are handled like Gun HTTP/2 trailers, so callers get raw trailer bytes as a stream item instead of decoded trailers at the end of the stream. Mint has the same embedded-trailer bug, but Dialyzer does not flag it because codec.decode/2 accepts any.

::warning file=lib/grpc/client/adapters/gun.ex,line=435,title=guard_fail::The guard clause can never succeed.

Root cause

The Channel and Stream structs support incrementally populating their values, but the public types describe only fully populated state. Type specs exist, but without a CI step checking them, this drift is not caught when implementation changes.

The GRPC.Client.Connection warnings above started in PR #527, where load balancing started picking full Channel structs per request. That would have been caught by Dialyzer in CI.

For client adapter response streams, embedded gRPC trailers and adapter trailer messages are different events. Embedded gRPC trailers are carried in DATA frame payloads. Gun and Mint both have adapter-level trailer handling for HTTP/2 trailers, but embedded trailers need to be decoded from the DATA payload first. Gun sends binary trailer payloads through the HTTP/2 trailer branch. Mint sends the same binary payloads through codec.decode/2 as if they were response messages.

Fix

Run Dialyzer for grpc in a separate CI job:

  • Add dialyxir to grpc
  • Cache deps/_build artifacts and Dialyzer PLTs, based on elixir-mint's CI setup

Fix existing violations to make Dialyzer pass:

  • Update GRPC.Channel.t() and GRPC.Client.Stream.t() to match values returned by client code
  • Decode embedded trailer frames in Gun (and Mint for parity) server streams

Review attention

This PR also exposes grpc_core helpers for embedded trailer payload/frame handling, to avoid low-level gRPC internals like the 0b1000_0000 trailers flag leaking into adapter code. However, grpc consumes grpc_core through the package dependency, so the Gun and Mint adapters cannot use the new core helper until the next publish of grpc_core. TODOs mark that temporary split.

Test plan

  • Added Gun regression test for embedded trailers delivered in :nofin and :fin data
  • Added Mint regression test for embedded trailers delivered in data
  • Added grpc_core tests for parsing and encoding embedded trailers

smartinio added 2 commits July 1, 2026 19:25
Expose helpers for parsing and encoding embedded trailer frames.
Run Dialyzer in CI for grpc. Update channel and stream types to match values returned by client code, and fix the Gun embedded trailer branch instead of widening types around it.
@polvalente polvalente closed this Jul 1, 2026
@polvalente

Copy link
Copy Markdown
Contributor

Thanks, but I don't think we should support Dialyzer anymore. The proper fix for us is to run Elixir 1.20 warnings as errors and to remove all explicit @SPEC annotations.

@smartinio

Copy link
Copy Markdown
Contributor Author

@polvalente Fair enough. Opened one for that in #552

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants