Repository navigation
gRPC service path doesn't propagate saga-id — header-identified sagas can't be started/continued over gRPC #3385
Description
Activity
Will dig into this further soon. Began scaffolding some ideas locally.
Findings after cloning the HTTP saga tests
Following the suggestion to mirror the existing HTTP endpoint + saga tests over gRPC, here's what shook out. The short version: sagas work over gRPC the same way they work over HTTP, and the "gap" in the title is real but narrower than it first read — it's the same limitation HTTP endpoints have.
Message-identified sagas already work over gRPC ✅
A saga is just a
HandlerChain, and the gRPC shim forwards to the sameIMessageBus.InvokeAsyncpipeline a[WolverinePost]endpoint uses. So a saga whose id rides on the request DTO ([SagaIdentity],OrderId,Id, or a conventional{Saga}Idmember) starts and continues over gRPC with zero gRPC-specific code.PR #3386 adds a test that clones
building_a_saga_and_publishing_other_messages_from_http_endpointover a gRPC hop — start → persist → continue → complete → state deleted — and it's green. Persistence is asserted via the in-memory saga persistor, so no database is needed.Header-identified sagas are the same limitation HTTP has
⚠️ The only case that breaks is a saga whose id is resolved from the envelope
saga-idheader (no id member on the message →PullSagaIdFromEnvelopeFrame). Nothing carriessaga-idacross a gRPC service hop:- The propagation interceptors carry
correlation-id/tenant-idbut notsaga-id. Executor.InvokeAsync<T>seedsTenantIdonto the invoked envelope but not a saga id.
…so
envelope.SagaIdis empty and you getIndeterminateSagaStateIdException(currently surfaced to the client as an opaqueStatusCode.Internal). PR #3386 pins this with a characterization test.Worth calling out: HTTP endpoints don't exercise the
saga-idheader path either. That header path is a node-to-node messaging/transport concern (a saga cascades a message, the broker carriessaga-idon the envelope) — not an endpoint concern. So "works like HTTP" is fully satisfied without addressing it.Proposed resolution
- Ship the parity tests (PR gRPC saga coverage: message-identified sagas work like HTTP; header-identified gap pinned (refs #3385) #3386) — proves the common case and gives regression coverage.
- Document that gRPC service calls, like HTTP endpoints, use message-body saga identity; the envelope
saga-idheader isn't propagated across a gRPC service hop. - Downscope this issue to either (a) a clearer diagnostic replacing the opaque
Internalwhen a header-id saga is invoked over gRPC, and/or (b) an optional future enhancement that propagatessaga-idsymmetrically (interceptors + a context→envelope seed inExecutor) if there's real demand.
Happy to go whichever direction on (3).
THoughts?
- The propagation interceptors carry
- added a commit that references this issue
on Jul 12, 2026 @erikshafer — thanks for the thorough write-up, and your read is right. Go with (a): the scoped diagnostic.
Your argument is the deciding one: message-identified sagas already work over gRPC (your #3386 proves it, and the docs page from #3389 now documents it as the supported path), and header identity is a transport concern that HTTP does not exercise either — so "works like HTTP" is already satisfied. Full three-point
saga-idpropagation is not worth the complexity until someone turns up with a concrete need for it.So the scope is:
- replace the opaque
IndeterminateSagaStateIdException/StatusCode.Internalover a gRPC hop with an actionable message — "a header-identified saga is not supported over a gRPC hop; put the saga identity on the message body", linking the gRPC + Sagas docs page; - flip the assertions in
starting_a_header_identified_saga_over_grpc_fails_with_opaque_status_todayso the characterization test pins the new diagnostic instead of the old opaque failure.
Targeting 6.18.0. If you would like to take it, it is yours — otherwise say so and we will pick it up. Leave (b) as a future issue if a real use case shows up.
- replace the opaque
- added a commit that references this issue
on Jul 13, 2026
Context
Following #3369 / #3382 (server-side envelope propagation + tenant detection for
Wolverine.Grpc, GH-3368), a question came up about whether starting a saga is similarly unsupported over the gRPC package. Tracing it, there's a real gap, but it's narrower than it first looks.What already works (no change needed)
A saga is just a
HandlerChain(SagaChain), and the gRPC shim forwards each RPC toIMessageBus.InvokeAsync<TResponse>/StreamAsync<T>. So sagas whose identity comes from the message body —[SagaIdentity],OrderId, or a conventionalIdon the request DTO — start and continue over gRPC today with zero gRPC-specific code.PullSagaIdFromMessageFramereads the id straight out of the deserialized request.The gap
Sagas with no id member on the message resolve identity from the envelope
saga-idheader viaPullSagaIdFromEnvelopeFrame(src/Wolverine/Persistence/Sagas/PullSagaIdFromEnvelopeFrame.cs:25,34), which throwsIndeterminateSagaStateIdExceptionwhenenvelope.SagaIdis empty. Across a gRPC service hop that value is never populated, for two independent reasons:saga-id. Client stampscorrelation-id/tenant-id/id/parent-id/conversation-id(WolverineGrpcClientPropagationInterceptor.cs:104-113); server reads back onlycorrelation-id/tenant-id(WolverineGrpcServicePropagationInterceptor.cs:109-119).Executor.InvokeAsync<T>(src/Wolverine/Runtime/Handlers/Executor.cs:166-181) copiesbus.TenantIdonto the invoked envelope but has no equivalent for saga id. So even a context-level saga id wouldn't reachPullSagaIdFromEnvelopeFrame. (This is why the tenant fix was cheap and this one isn't — the tenant copy already existed.)Fix surface
EnvelopeConstants.SagaIdKeyfromctx.Envelope?.SagaId.saga-id; stash on scoped context (needs a settable hook —MessageContext.SetSagaId(object)exists but isn't onIMessageContext).Executor.InvokeAsync<T>/StreamAsync<T>envelope.SagaIdfrom context, mirroring the existingTenantIdline.Proposed direction
Ship a scoped first cut: leave core
Executor/IMessageContextuntouched, keep documenting message-body identity as the supported path for gRPC-driven sagas, and replace the opaqueIndeterminateSagaStateIdExceptionwith a clear diagnostic when a header-id saga is invoked over gRPC. Full transparent propagation (all three points above) can follow if there's demand.Open question
Is the real-world need message-body identity (already works) or header identity? And is the scenario starting a saga from a gRPC call, or continuing one? That decides whether we do the scoped diagnostic or the full core change.
Reproduction
A characterization test that stands up a header-identified saga (no id member on the message) behind a code-first gRPC service on a plain in-memory host reproduces the failure in a single call — the RPC comes back
StatusCode.Internalwith detail "Could not determine a valid saga state id…". It slots alongsidesrc/Wolverine.Grpc.Tests/ServerPropagation/(thePropagationEchoGrpcServicefixture is the template). Current version: 6.17.2.