This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch worktree-fix+CAMEL-24365 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 45a0915d563bc101f1f8ea08e9f4c7c182d2c0b3 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Aug 6 15:18:24 2026 +0200 CAMEL-24365: Copy attachment trait from IN when creating a fresh OUT message Since CAMEL-21755 moved attachments from Exchange-level properties to Message-level traits, any producer that creates a new OUT message via exchange.getOut() silently drops attachments. Fix by copying the ATTACHMENTS trait in AbstractExchange.newOutMessage(), and remove the per-component workaround added in CAMEL-23193 for camel-jms. Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../attachment/AttachmentOnOutMessageTest.java | 75 ++++++++++++++++++++++ .../org/apache/camel/component/jms/JmsMessage.java | 7 -- .../org/apache/camel/support/AbstractExchange.java | 10 +++ 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/components/camel-attachments/src/test/java/org/apache/camel/attachment/AttachmentOnOutMessageTest.java b/components/camel-attachments/src/test/java/org/apache/camel/attachment/AttachmentOnOutMessageTest.java new file mode 100644 index 000000000000..520eaa06172a --- /dev/null +++ b/components/camel-attachments/src/test/java/org/apache/camel/attachment/AttachmentOnOutMessageTest.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.attachment; + +import jakarta.activation.DataHandler; + +import org.apache.camel.Exchange; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test that attachments on the IN message survive when a producer creates a fresh OUT message via exchange.getOut(). + * This is a regression test for CAMEL-24365. + */ +class AttachmentOnOutMessageTest extends CamelTestSupport { + + @Test + void testAttachmentSurvivesOutMessage() throws Exception { + getMockEndpoint("mock:result").expectedMessageCount(1); + + template.sendBody("direct:start", "Hello"); + + MockEndpoint.assertIsSatisfied(context); + + Exchange received = getMockEndpoint("mock:result").getReceivedExchanges().get(0); + AttachmentMessage am = received.getMessage(AttachmentMessage.class); + assertTrue(am.hasAttachments()); + assertEquals(1, am.getAttachmentNames().size()); + assertTrue(am.getAttachmentNames().contains("test.txt")); + } + + @Override + protected RoutesBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:start") + .process(exchange -> { + AttachmentMessage msg = exchange.getMessage(AttachmentMessage.class); + msg.addAttachment("test.txt", new DataHandler("content", "text/plain")); + }) + .process(exchange -> { + // simulate what HttpProducer does: create a fresh OUT message + exchange.getOut().setBody("response"); + }) + .process(exchange -> { + // after pipeline promotes OUT to IN, attachments must still be present + AttachmentMessage msg = exchange.getMessage(AttachmentMessage.class); + exchange.getMessage().setHeader("attachmentCount", msg.getAttachmentNames().size()); + }) + .to("mock:result"); + } + }; + } +} diff --git a/components/camel-jms/src/main/java/org/apache/camel/component/jms/JmsMessage.java b/components/camel-jms/src/main/java/org/apache/camel/component/jms/JmsMessage.java index e0d67a1f3596..38bf1c9cf1a3 100644 --- a/components/camel-jms/src/main/java/org/apache/camel/component/jms/JmsMessage.java +++ b/components/camel-jms/src/main/java/org/apache/camel/component/jms/JmsMessage.java @@ -17,7 +17,6 @@ package org.apache.camel.component.jms; import java.io.File; -import java.util.LinkedHashMap; import java.util.Map; import jakarta.jms.Destination; @@ -121,12 +120,6 @@ public class JmsMessage extends DefaultMessage { if (that.hasHeaders()) { getHeaders().putAll(that.getHeaders()); } - - // copy attachments - Map<String, Object> attachments = (Map<String, Object>) that.getPayloadForTrait(MessageTrait.ATTACHMENTS); - if (attachments != null) { - setPayloadForTrait(MessageTrait.ATTACHMENTS, new LinkedHashMap<>(attachments)); - } } public JmsBinding getBinding() { diff --git a/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java b/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java index 10cc6ee63742..ce4bf980344b 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java @@ -19,6 +19,7 @@ package org.apache.camel.support; import java.util.ArrayList; import java.util.EnumMap; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -514,10 +515,19 @@ abstract class AbstractExchange implements Exchange, ExchangeExtension { return out; } + @SuppressWarnings("unchecked") private Message newOutMessage() { if (in != null) { Message answer = in.newInstance(); CamelContextAware.trySetCamelContext(answer, getContext()); + // copy attachments from IN so they survive when a producer swaps in a fresh OUT + if (in.hasTrait(MessageTrait.ATTACHMENTS)) { + Map<String, Object> attachments + = (Map<String, Object>) in.getPayloadForTrait(MessageTrait.ATTACHMENTS); + if (attachments != null) { + answer.setPayloadForTrait(MessageTrait.ATTACHMENTS, new LinkedHashMap<>(attachments)); + } + } return answer; } else { return new DefaultMessage(getContext());
