Skip to content

Fix header copy aliasing that breaks message delivery - #1627

Closed
gvdutra wants to merge 1 commit into
nats-io:mainfrom
gvdutra:fix/header-copy-isolation
Closed

gvdutra wants to merge 1 commit into
nats-io:mainfrom
gvdutra:fix/header-copy-isolation

Conversation

@gvdutra

@gvdutra gvdutra commented Sep 7, 2026

Copy link
Copy Markdown

Copying Headers and appending a value for an existing key can prevent subsequent messages from being delivered. The copy constructor copies the map but shares its mutable value lists. Updating either instance changes both sets of values, while only updating one instance's length bookkeeping.

This affects applications that copy a reusable header template and extend it for an individual message. It also allows changes to the source to alter a read-only copy.

Reproduction

With a connected Connection nc and a running NATS server:

Subscription sub = nc.subscribe("header.copy");
Headers template = new Headers().add("Trace", "base");
Headers extended = new Headers(template).add("Trace", "derived");
byte[] body = "payload".getBytes(StandardCharsets.UTF_8);

nc.publish("header.copy", template, body);
nc.publish("header.copy", extended, body);
Message first = sub.nextMessage(2000);

Expected: both messages arrive with the original payload; the first has Trace: base, and the second has both base and derived values.

Before the fix, extending the copy also changes template, leaving its recorded header length too small. The client reports IllegalArgumentException: Invalid header composition, and first is null. This was reproduced against a real NATS server on main at f83ba05d.

Change

Copy each value list in the Headers copy constructor so the source and destination have independent mutable state. Preserve the existing map sizing, length bookkeeping, read-only flag, and excluded-key handling.

Regression coverage checks:

  • Adding to a copy leaves the source values and serialized length intact.
  • Adding to the source leaves both writable and read-only copies intact, including copies with excluded keys.
  • Publishing the original and extended headers delivers both messages with the expected headers and payloads.

Validation

All four new regression cases fail without the fix. With the fix, all 61 tests in the targeted suites pass, with no failures or skips:

./gradlew test \
  --tests '*HeadersTests' \
  --tests 'io.nats.client.PublishTests' \
  --tests '*NatsMessageTests'

Tested with Temurin JDK 21.0.11 and NATS Server 2.14.6 on macOS arm64. git diff --check passes. The full test suite was not run.

@gvdutra
gvdutra marked this pull request as draft September 7, 2026 01:33
@gvdutra gvdutra closed this Sep 7, 2026
@scottf

scottf commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Just to let you know I've seen this. It would be very expensive to ensure a deep copy. There is no contract that says a header is reusable, this is a known limitation.

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.

2 participants