Skip to content

fix(transit/message): yield legit JSON null payload in ReceiveAllAsync (fixes #220)#263

Merged
Kiryuumaru merged 2 commits into
masterfrom
fix/receiveall-null-sentinel
May 21, 2026
Merged

fix(transit/message): yield legit JSON null payload in ReceiveAllAsync (fixes #220)#263
Kiryuumaru merged 2 commits into
masterfrom
fix/receiveall-null-sentinel

Conversation

@Kiryuumaru
Copy link
Copy Markdown
Owner

Problem (fixes #220)

MessageTransit<TSend, TReceive>.ReceiveAllAsync overloaded null as a sentinel for two distinct conditions:

  1. End-of-streamReceiveAsync returns default after the length-prefix read returns 0 bytes
  2. A peer-sent JSON null payload — 4-byte length prefix + 6E 75 6C 6C; deserializes to null for nullable TReceive

The receive loop checked if (_receiveEof || message is null) yield break;. The value-type half of this bug was previously fixed (#177) by adding the explicit _receiveEof flag, but the secondary message is null clause was kept and still terminates the stream on the first legitimate JSON null message. Every subsequent peer message is silently lost — no exception, no event, no log.

Example (pre-fix): peer sends "hello", null, "world". Consumer's await foreach over ReceiveAllAsync() yields only "hello" and exits. "world" is permanently dropped.

Fix

Drop the secondary message is null terminator. _receiveEof is the sole signal — and it is reliably set inside ReceiveAsync whenever the length-prefix or payload read returns 0 bytes. The yield is message! because a legit JSON null payload is a valid yielded value for nullable TReceive.

if (_receiveEof)
    yield break;

yield return message!;

No public-API change.

Regression Test

Added in tests/NetConduit.Transit.Message.UnitTests/MessageTransitTests.cs under #region ReceiveAllAsync:

  • MessageTransit_ReceiveAllAsync_YieldsLegitimateJsonNullPayload_AndContinuesMessageTransit<string?, string?> sends "hello", null, "world"; asserts the receiver yields all three in order before EOF.

Pre-fix verification (stashed the prod change, ran the new test):

MessageTransit_ReceiveAllAsync_YieldsLegitimateJsonNullPayload_AndContinues [FAIL]
  Assert.Equal() Failure: Collections differ
                                 ↓ (pos 1)
  Expected: ["hello", null, "world"]
  Actual:   ["hello"]

Post-fix: passes.

Existing tests MessageTransit_ReceiveAllAsync_StopsOnChannelClose and ..._WhenReceiveTypeIsValueType (covering EOF for reference and value-type TReceive respectively, #177) continue to pass — _receiveEof correctly drives termination for both cases.

Verification

  • dotnet build -c Debug — 0 warnings, 0 errors across net8.0 / net9.0 / net10.0
  • dotnet test -c Debug --no-build — 635 passed / 1 known-flake (PublicApiTests.OnChannelOpened_DoesNotFire_BeforeRemoteAccept, passes in isolation) / 1 pre-existing skip (MemoryLeak_SubMuxChaos_MemoryStaysBounded)
  • Targeted re-run of the flake in isolation: 1 passed

@Kiryuumaru Kiryuumaru enabled auto-merge (squash) May 21, 2026 07:02
@Kiryuumaru Kiryuumaru disabled auto-merge May 21, 2026 07:02
@Kiryuumaru Kiryuumaru merged commit 96e37f9 into master May 21, 2026
14 checks passed
@Kiryuumaru Kiryuumaru deleted the fix/receiveall-null-sentinel branch May 21, 2026 07:03
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.

MessageTransit.ReceiveAllAsync terminates on legit JSON null and infinite-loops for value-type TReceive — overloaded null sentinel

1 participant