Skip to content

grpc: accept legacy stream protocol clients via Meta("grpc:stream:compat", "v1") - #3946

Merged
raphael merged 1 commit into
v3from
grpc-stream-compat
Jul 2, 2026
Merged

grpc: accept legacy stream protocol clients via Meta("grpc:stream:compat", "v1")#3946
raphael merged 1 commit into
v3from
grpc-stream-compat

Conversation

@raphael

@raphael raphael commented Jul 2, 2026

Copy link
Copy Markdown
Member

What this does

Adds an opt-in compatibility mode that lets generated gRPC servers accept clients speaking the pre-envelope stream protocol, alongside clients speaking the current one.

Context: #3918 changed the wire contract for gRPC methods that define both Payload(...) and StreamingPayload(...). The one-shot payload used to be stringified into gRPC request metadata; it is now sent as a typed initial_payload frame inside a streamed request envelope. That change is wire-breaking: an old client cannot talk to a new server for those methods, and vice versa. Without this PR, fleets with services that call each other over such methods have to upgrade both sides of every call edge simultaneously.

With this PR, a single design annotation makes the regenerated server understand both protocols, turning the migration into an ordinary rolling upgrade: servers first, clients whenever convenient, no coordinated cutover.

How to use it

  1. Add the meta to the affected method (or to the service or API to cover all of its methods) and regenerate:

    Method("log", func() {
        Meta("grpc:stream:compat", "v1")
        Payload(LogContext)          // one-shot payload
        StreamingPayload(LogEntry)   // streamed items
        GRPC(func() {})
    })
  2. Deploy the regenerated servers. They now accept old and new clients alike.

  3. Upgrade clients at your own pace. Clients regenerated with current Goa automatically speak the envelope protocol and identify themselves (see below); old clients keep working unchanged.

  4. Once no legacy callers remain, delete the meta and regenerate. All compatibility code disappears from gen/.

How it works

  • Protocol negotiation. Generated clients of envelope methods now always append one request metadata entry, goa-stream-protocol: 2 (constants live in the goagrpc runtime package). Legacy clients — generated by pre-envelope Goa — send no such key. The generated server checks the key once per stream (goagrpc.UsesStreamEnvelope); sniffing frame bytes would be ambiguous, the explicit key is not.
  • Envelope path (key present). Identical to today: the handler reads the initial frame, the decoder unwraps the initial_payload oneof branch, Recv() unwraps stream_item frames.
  • Legacy path (key absent). The handler skips the initial read (the legacy protocol's first frame is already a stream item). A dedicated generated decoder, decode<Method>LegacyRequest, rebuilds the payload from request metadata exactly as pre-codegen: carry mixed gRPC streaming requests in typed envelopes #3918 servers did: one entry per payload attribute, the reserved goa_payload key for non-object payloads, and a New<Method>PayloadFromMetadata constructor. The server stream carries a legacy flag so Recv() reads raw <Method>StreamItem frames via RecvMsg — same field tags as the old stream message, so old frames parse as-is.
  • Wire contract unchanged. The generated .proto is identical with or without the meta; both message types it needs already exist. Explicit GRPC.Metadata(...) attributes and security metadata travel in metadata under both protocols and are shared.

Constraints and validation

  • The legacy protocol can only encode primitives and arrays of primitives in metadata, so the meta is validated in the design: payloads with nested objects, maps, or unions are rejected with a clear error (expr validation). This is not a new limitation — such payloads never worked pre-codegen: carry mixed gRPC streaming requests in typed envelopes #3918 either; new designs should simply not set the meta.
  • The meta only fixes the old-client → new-server direction. A new client still cannot call a pre-envelope server, so the rollout order is servers before clients, per call edge.
  • Only supported value is "v1"; setting it on a method without both Payload and StreamingPayload is a design error (service/API-level metas silently skip non-streaming methods, as intended).

Testing

  • Codegen goldens for object, primitive (goa_payload), and mixed explicit-metadata payloads; the generated legacy decoder is shape-identical to real pre-codegen: carry mixed gRPC streaming requests in typed envelopes #3918 goldens.
  • expr validation tests for meta value, placement, cascade (method/service/API) and payload-encodability errors.
  • Runtime unit test for goagrpc.UsesStreamEnvelope.
  • End-to-end: a generated server exercised by (a) the generated envelope client and (b) a simulated legacy client sending payload metadata plus raw frames without the protocol key — covering object, primitive, and mixed-metadata methods.
  • make lint clean, full make test green. The only changes to pre-existing goldens are the two envelope request encoders gaining the protocol-declaration line.

Docs

  • dsl/meta.go documents the new meta key.
  • grpc/docs/FAQ.md gains a migration recipe ("How do I upgrade services that still have clients using the pre-envelope stream protocol?").

Add opt-in backward compatibility for the pre-envelope gRPC stream
protocol so services can adopt the typed stream envelope introduced in
#3918 without a synchronized client/server cutover.

Setting Meta("grpc:stream:compat", "v1") on a method, service or API
makes generated servers accept both protocols for methods that combine
Payload and StreamingPayload: generated clients now declare the
envelope protocol through the goa-stream-protocol request metadata key
and requests without the key are decoded using the legacy contract,
payload in request metadata (goa_payload for primitives) and raw
stream item frames. The .proto definition is unchanged.
@raphael
raphael merged commit 042efad into v3 Jul 2, 2026
5 checks passed
@raphael
raphael deleted the grpc-stream-compat branch July 2, 2026 22:49
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.

1 participant