Preserve MessageHeaderAccessor binding in spring-integration TracingChannelInterceptor#19303
Conversation
…hannelInterceptor preSend() rebuilds the message so it can carry the injected trace context. It did so with MessageBuilder.fromMessage(..).copyHeaders(..).build(), which produces plain MessageHeaders. That drops the MutableMessageHeaders binding between the message and its MessageHeaderAccessor, so components that recover the accessor via MessageHeaderAccessor.getAccessor() no longer find it. StompBrokerRelayMessageHandler is one such component: it requires the accessor and throws "No header accessor (not using the SimpMessagingTemplate?)" when it is missing, which makes STOMP broker relaying fail while this instrumentation is enabled. Rebuilding now distinguishes three cases. A message with no bound accessor, and any ErrorMessage, keep the existing MessageBuilder path; ErrorMessage does so because from 5.3 on MessageBuilder carries its originalMessage over into the rebuilt message and createMessage cannot. When the message's headers are mutable, getMutableAccessor() returns the message's own accessor and the context has already been written in place, so the message is returned unchanged; rebuilding it would seal the caller's message. Otherwise the accessor copy is sealed and its own MessageHeaders are handed to the new message, which preserves the binding while still giving the message an id and preventing it from sharing mutable headers with the accessor. Adds AbstractTracingChannelInterceptorTest covering context propagation, payload and header preservation, message id, header immutability, ErrorMessage type and originalMessage, STOMP native headers, and accessor preservation for mutable and immutable messages, over both a direct and an executor channel, from the library and javaagent test modules. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One or more co-authors of this pull request were not found. You must specify co-authors in commit message trailer via: Supported
Alternatively, if the co-author should not be included, remove the Please update your commit message(s) by doing |
Pull request dashboard statusStatus last refreshed: 2026-07-23 05:19:36 UTC.
This automated status or its linked feedback items may be incorrect. If something looks wrong, please report it with the result you expected. If you believe this pull request is incorrectly routed as waiting on the author, comment |
There was a problem hiding this comment.
Pull request overview
Preserves Spring MessageHeaderAccessor bindings when injecting trace context, preventing downstream STOMP handling failures.
Changes:
- Handles mutable, immutable, unbound, and
ErrorMessagecases appropriately. - Adds shared tests for accessor, header, payload, context, and message preservation.
- Runs the tests for both library and Java agent instrumentation.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
testing/.../AbstractTracingChannelInterceptorTest.java |
Adds shared regression coverage. |
library/.../TracingChannelInterceptorTest.java |
Enables shared library tests. |
library/.../TracingChannelInterceptor.java |
Preserves accessor bindings during message reconstruction. |
javaagent/.../TracingChannelInterceptorTest.java |
Enables shared Java agent tests. |
Fixes #6855 (closed by the reporter once they realized they could just disable the feature to stop it from breaking their system)
Problem
With spring-integration instrumentation enabled, Spring components that recover a
message's
MessageHeaderAccessorstop finding it.StompBrokerRelayMessageHandleris one such component — it requires the accessor and throws:
which breaks STOMP broker relaying. The usual workaround is
otel.instrumentation.spring-integration.enabled=false.Root cause
preSend()rebuilds the message so it can carry the injected trace context:toMessageHeaders()returns plainMessageHeaders. The link between a message andits accessor lives in
MutableMessageHeaders, so rebuilding this way silently dropsit and
MessageHeaderAccessor.getAccessor(message, ...)returnsnulldownstream.Fix
Rebuilding now distinguishes three cases:
ErrorMessage— keep the existingMessageBuilderpath.
ErrorMessagedoes so because from 5.3 onMessageBuildercarries itsoriginalMessageover into the rebuilt message andcreateMessagecannot.getMutableAccessor()returns the message's own accessor,so the context has already been written in place; return the message unchanged.
Rebuilding here would seal the caller's message.
MessageHeaders.Sealing gives the new message an
idand stops it sharing mutable headers withthe accessor, while
createMessagepreserves the binding.All APIs used are public at the 4.1.2 compile floor.
Tests
AbstractTracingChannelInterceptorTestin thetestingmodule, with thin subclassesin
libraryandjavaagent, following the existingAbstractSpringIntegrationTracingTestpattern. Messages are sent through a real channel and asserted on what the handler
receives, over both a direct and an executor channel.
Covers context propagation, payload/header preservation, message
id, headerimmutability, cross-send header isolation,
ErrorMessagetype andoriginalMessage,STOMP native headers, and accessor preservation for mutable and immutable messages.
Each of the three branches was removed in turn to confirm a test fails for each.
Verification
library/javaagent/testingchecktestLatestDeps(5.5.20), both modulestestLatestDepsis capped at5.+by a documented limitation, so muzzle is the onlycoverage of 6.x/7.x. The
assertInverseboundary is unmoved at 4.1.0.Behaviour notes
Timestamp. On the sealed path the rebuilt message has no
timestamp, where theprevious code generated one.
setEnableTimestampisprotectedso it cannot berestored externally. This matches Spring's own idiom — accessor-bound messages built
through
createMessagecarry no timestamp, and the input messages in this shape don'teither — but it is a change. The message
idis present; for mutable accessor-boundmessages
idwas already absent before this change.Accessor subclasses. Only subclasses that override
createAccessorare restored,because
getMutableAccessordelegates to it. Every Spring accessor intended to survivea mutable copy does so; a subclass that doesn't will still resolve to the base type.
ErrorMessage.
originalMessagepreservation is guaranteed structurally rather thanby assertion at the baseline: neither the constructor that populates it nor
getOriginalMessage()exists at the 4.1.2 compile floor, so that test isreflection-based and gated on
testLatestDeps().Not included
A test driving
StompBrokerRelayMessageHandlerend to end. Its accessor check sitsbehind an
isBrokerAvailable()guard, so without a live broker connection the handlerreturns early and such a test would pass vacuously. The
StompHeaderAccessorscenariocovers the precondition instead, including the native headers a STOMP frame actually
transmits. Happy to add a broker-backed test with guidance.