Deepak created CAMEL-24398:
------------------------------
Summary: [BUG] DefaultAttachmentMessage.hasAttachments() returns
true for messages with no attachments in Camel 4.x
Key: CAMEL-24398
URL: https://issues.apache.org/jira/browse/CAMEL-24398
Project: Camel
Issue Type: Bug
Components: camel-attachments
Affects Versions: 4.14.2
Reporter: Deepak
*Summary*
hasAttachments() incorrectly returns true on a plain (non-multipart) message in
Camel 4.x. This is a regression from
Camel 3.x introduced during the MessageTrait refactor. It causes
MimeMultipartDataFormat.marshal() to take the
multipart code path for plain XML messages, corrupting the message body.
*Component*
camel-attachments — org.apache.camel.attachment.DefaultAttachmentMessage
---
*Steps to Reproduce*
1. Configure an HTTP Sender adapter with a MimeMultipartDataFormat marshal
step downstream.
2. POST a plain XML payload (no multipart, no attachments) to the endpoint.
3. Observe that MimeMultipartDataFormat.marshal() takes the multipart path
instead of writing the body as-is.
---
*Root Cause*
The MessageTrait refactor in Camel 4.x introduced a lazy-init pattern in
getAttachmentsMap() that eagerly registers
the ATTACHMENTS trait with an empty LinkedHashMap the first time any
attachment-inspection method is called (e.g.
getAttachments(), getAttachmentNames()). Once registered, hasAttachments()
returns true because it only checks for
trait presence — not whether the map is empty.
*Call chain for a plain XML POST:*
DefaultHttpBinding.readRequest()
→ readBody()
→ populateAttachments() // called unconditionally for every
request
→ getMessage(AttachmentMessage.class)
→ AttachmentConverter.toAttachmentMessage()
→ new DefaultAttachmentMessage(message)
→ getAttachmentsMap()
→ setPayloadForTrait(ATTACHMENTS, emptyLinkedHashMap) // ←
trait registered with empty map
... later ...
MimeMultipartDataFormat.marshal()
→ exchange.getIn(AttachmentMessage.class).hasAttachments()
→ delegate.hasTrait(ATTACHMENTS) // ← returns true! empty map was
registered above
→ takes multipart path // ← wrong for plain XML
---
*Code Comparison*
Camel 3.14.7 — DefaultAttachmentMessage.hasAttachments() ✅ Correct
@Override
public boolean hasAttachments() {
Map<String, Attachment> map =
getExchange().getProperty(ATTACHMENT_OBJECTS, Map.class);
return map != null && !map.isEmpty(); // checks both null AND emptiness
}
Attachments were stored as an Exchange property. Reading the property when
nothing was added returns null, so
hasAttachments() correctly returns false.
---
Camel 4.14.2 — DefaultAttachmentMessage.hasAttachments() ❌ Broken
@Override
public boolean hasAttachments() {
return delegate.hasTrait(MessageTrait.ATTACHMENTS); // only checks trait
key presence
}
Combined with the lazy-init in getAttachmentsMap():
private Map<String, Object> getAttachmentsMap() {
var m = (Map<String, Object>)
delegate.getPayloadForTrait(MessageTrait.ATTACHMENTS);
if (m == null) {
m = new LinkedHashMap<>();
delegate.setPayloadForTrait(MessageTrait.ATTACHMENTS, m); //
registers trait even when empty
}
return m;
}
Any read-only call to getAttachments(), getAttachmentNames(), or
getAttachmentObjects() silently registers the
ATTACHMENTS trait with an empty map, causing all subsequent hasAttachments()
calls to return true.
Observed vs Expected
{*}Expected{*}: hasAttachments() returns false when no attachments have been
added
{*}Actual{*}: hasAttachments() returns true after any read-only attachment
inspection, even with zero attachments
--------------------
*Debugger Evidence*
Exchange traits map at MimeMultipartDataFormat.marshal() breakpoint:
traits = \{EnumMap size=1}
MessageTrait.ATTACHMENTS → \{LinkedHashMap size=0} ← trait present, but
map is EMPTY
--
This message was sent by Atlassian Jira
(v8.20.10#820010)