Skip to content

codegen: carry mixed gRPC streaming requests in typed envelopes#3918

Merged
raphael merged 1 commit intov3from
codegen/grpc-stream-envelope
Apr 12, 2026
Merged

codegen: carry mixed gRPC streaming requests in typed envelopes#3918
raphael merged 1 commit intov3from
codegen/grpc-stream-envelope

Conversation

@raphael
Copy link
Copy Markdown
Member

@raphael raphael commented Apr 12, 2026

Problem

Fixes #3869.

Today, gRPC methods that define both Payload(...) and StreamingPayload(...) do not preserve the ordinary method payload as a typed request message. During endpoint finalization, Goa rewrites that payload into request metadata instead.

That transport contract is problematic for a few reasons:

  • it is implicit rather than user-directed
  • it happens after validation, so the synthesized metadata shape is not validated as such
  • it forces ordinary payload data through a stringly metadata channel instead of the typed protobuf message channel
  • it breaks rich payload shapes such as unions, objects, and maps, which is what triggered the OneOf repro in OneOf Type in Streaming Payload Generates Invalid Code #3869

The concrete failure mode in the repro is that a union payload field paired with StreamingPayload(...) generates invalid gRPC code because the payload is treated as metadata even though the metadata path only really supports primitive-like string conversions.

Design Goal

The goal here is not to patch the metadata path to support more cases. That would preserve the wrong abstraction.

Instead, this change makes the gRPC contract conceptually match the service contract:

  • the ordinary method payload remains a one-shot typed payload
  • the streaming payload remains the incremental stream item type
  • gRPC metadata is reserved for explicit GRPC.Metadata(...) declarations and security/auth fields

For gRPC methods that define both Payload(...) and StreamingPayload(...), the transport now uses a typed streamed request envelope with two variants:

  • initial_payload
  • stream_item

That keeps all non-metadata request data in the protobuf message channel and aligns gRPC more closely with how Goa already models this shape in other streaming transports.

What Changed

Endpoint finalization

In expr/grpc_endpoint.go:

  • removed the implicit Payload -> Metadata promotion for methods with StreamingPayload(...)
  • kept the existing explicit metadata path and security metadata inference
  • left the ordinary request payload as a real request message shape after finalization

This is the root contract change.

Protobuf/codegen model

In grpc/codegen/service_data.go and the gRPC templates:

  • synthesized a typed streamed request envelope when both payload kinds are present
  • kept separate knowledge of:
    • the one-shot payload message
    • the stream item message
    • the transport-level streamed envelope
  • reused the existing protobuf transforms for both branches instead of introducing JSON codecs or metadata stringification

The generated protobuf stream request now carries:

  • one initial_payload frame to open the logical request
  • zero or more stream_item frames for the actual stream contents

Client behavior

Updated the client open/send path so that:

  • opening a mixed streaming method sends the initial payload frame once
  • subsequent Send(...) calls emit only stream_item envelope frames
  • service-level client APIs still look the same from the caller's perspective

Server behavior

Updated the server path so that:

  • the first streamed frame is read and decoded before endpoint invocation
  • the endpoint still receives EndpointInput{Payload, Stream}
  • the generated stream wrapper unwraps the envelope and exposes only typed stream items to service code via Recv()

So the service contract stays stable even though the transport contract becomes correct.

Why This Design

This is a fairly deep change, but it simplifies the model rather than adding more special cases.

Benefits:

  • ordinary payload no longer leaks into headers just because a method also streams items
  • unions, objects, and maps use the normal protobuf machinery instead of ad hoc metadata conversions
  • the transport representation is explicit and typed
  • gRPC metadata semantics become narrower and clearer
  • generated code matches the conceptual service contract more closely

This also avoids growing more metadata-specific encode/decode logic for cases that were never really metadata in the first place.

Behavioral Impact

This intentionally changes the gRPC wire contract for methods that define both Payload(...) and StreamingPayload(...).

What stays the same:

  • explicit GRPC.Metadata(...) fields still use metadata
  • security/auth attributes still use metadata as before
  • the service-facing API shape remains one-shot Payload plus typed stream item send/recv

What changes:

  • ordinary request payload fields are no longer transported via request metadata for mixed streaming methods
  • mixed streaming methods now use a typed request envelope on the stream

That is a deliberate contract correction rather than a backward-compatible patch.

Tests And Validation

Added/updated coverage for:

  • endpoint finalization behavior in expr/grpc_endpoint_test.go
  • a direct mixed-streaming regression fixture in expr/testdata/endpoint_dsls.go
  • gRPC streaming codegen coverage in grpc/codegen/streaming_test.go
  • a union-payload mixed-streaming repro in grpc/codegen/testdata/dsls.go
  • affected gRPC goldens under grpc/codegen/testdata/golden
  • DSL and transport docs in dsl/payload.go, dsl/grpc.go, and grpc/docs/FAQ.md

Test Plan

  • go test ./expr ./grpc/codegen ./dsl
  • make

Stop rewriting ordinary payload into metadata for methods that also stream payload items. Generate a typed initial-payload/stream-item envelope and update the gRPC tests and docs around the new transport contract.
@raphael raphael merged commit 36883b3 into v3 Apr 12, 2026
7 checks passed
@raphael raphael deleted the codegen/grpc-stream-envelope branch April 12, 2026 18:50
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.

OneOf Type in Streaming Payload Generates Invalid Code

1 participant