Skip to content

GH-4595: stop the protocol version header surviving a round trip - #4599

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-4595-redis-duplicate-protocol-header
Sep 24, 2026
Merged

jeremydmiller merged 1 commit into
mainfrom
gh-4595-redis-duplicate-protocol-header

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #4595.

RedisEnvelopeMapper wrote wolverine-wolverine-protocol-version twice whenever it re-sent an envelope it had received. The round trip is the cause, not the Redis mapper: writeIncomingHeaders copies every wire header into Envelope.Headers, so the received envelope kept wolverine-protocol-version as a header of its own, and MapEnvelopeToOutgoing stamps its own copy onto every send. Second pass, two fields with the same name.

Where it bites

Redis's outgoing headers are a List<NameValueEntry> and Kafka's are a Headers list — both accept the same name twice. Everywhere else the outgoing headers are a dictionary, so the second write collapsed onto the first and nothing showed. So the visible damage is a malformed stream entry, most easily reached through the native dead letter queue under BufferedInMemory or Durable listening (those route the failure through TryBuildDeadLetterSender → the mapper, rather than through RedisStreamListener.MoveToErrorsAsync, which writes its fields explicitly). A consumer reading that entry into a keyed structure throws:

System.ArgumentException : An item with the same key has already been added.
Key: wolverine-wolverine-protocol-version

This is latent on main only because the Redis DeadLetterQueueTests currently listen Inline. It surfaced in CI on #4506, which is not its cause.

The fix

One line, in EnvelopeMapper.MapIncomingToEnvelope: drop the protocol version from Envelope.Headers as the envelope comes in.

Central rather than per-transport, because the exposure is not Redis's. Nothing in the codebase reads the protocol version off Envelope.Headers — it is plumbing that the outgoing mapper re-stamps on every send — and every mapper that copies wire headers wholesale keeps it. Kafka had the identical duplicate; the test proves both.

Tests

envelope_mapper_round_trip_4595 in each of the Redis and Kafka test suites. No broker: they map an envelope out, read it back, and map it out again, exactly as the dead letter sender does. Red before the change (3 of 4 Redis, 2 of 2 Kafka), green after, and red again with the one line commented out.

Clean dotnet build wolverine.slnx -c Release -f net9.0.

Not fixed here

The issue's closing observation stands and is worth its own issue: the two Redis dead-letter paths write different field shapes — MoveToErrorsAsync writes exception-type / exception-message / failed-at and friends, the dead letter sender path writes mapper-shaped wolverine-* fields. Whether native dead letter metadata should depend on the listener mode at all is the larger question behind this bug, and is not something to settle inside a one-line fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj

writeIncomingHeaders copies every wire header into Envelope.Headers, so an envelope
received from Redis or Kafka carried wolverine-protocol-version as a header of its own.
MapEnvelopeToOutgoing stamps its own copy on every send, so re-sending that envelope --
the dead letter sender path under BufferedInMemory or Durable listening, most visibly --
wrote the field twice. A dictionary-shaped outgoing header set silently collapsed the two;
Redis (List<NameValueEntry>) and Kafka (Headers) both accept the same name twice, so the
entry landed malformed and any consumer reading it into a keyed structure threw.

Fixed centrally in MapIncomingToEnvelope rather than per transport: nothing reads the
protocol version off Envelope.Headers, it is plumbing the outgoing mapper re-stamps, and
every mapper that copies headers wholesale has the same exposure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj
@jeremydmiller
jeremydmiller merged commit c8c997c into main Sep 24, 2026
44 checks passed
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.

Redis: RedisEnvelopeMapper duplicates the protocol version header when re-sending a received envelope

1 participant