This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new c0177a5a77a0 CAMEL-26132: camel-rss - Fail the exchange when the feed
cannot be cloned for a split entry (#28908)
c0177a5a77a0 is described below
commit c0177a5a77a004366a88831a1a1075bd1d4300c4
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:41:26 2026 +0100
CAMEL-26132: camel-rss - Fail the exchange when the feed cannot be cloned
for a split entry (#28908)
…oned for a split entry
With splitEntries=true, RssEndpoint.createExchange clones the SyndFeed to
hold the one entry. When the clone failed with CloneNotSupportedException it
only logged at DEBUG and sent the exchange with a null body, so the entry
was
lost without a visible error. Now it logs at WARN, keeps the SyndEntry as
the
body and sets a RuntimeCamelException on the exchange, so the route's error
handler (for example a dead letter channel) receives the entry.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../apache/camel/catalog/docs/rss-component.adoc | 5 +
.../camel-rss/src/main/docs/rss-component.adoc | 5 +
.../apache/camel/component/rss/RssEndpoint.java | 12 +-
.../component/rss/RssEntryCloneFailureTest.java | 136 +++++++++++++++++++++
4 files changed, 153 insertions(+), 5 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rss-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rss-component.adoc
index 3d7fdeb1d784..b7fb1f079906 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rss-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rss-component.adoc
@@ -62,6 +62,11 @@ a `SyndFeed` with one `SyndEntry` or the whole `SyndFeed`
with all its `SyndEntr
|`splitEntries` |`false` |The entire list of entries from the current feed is
set in the exchange.
|=======================================================================
+With `splitEntries=true`, Camel copies the feed (with ROME's `clone`) to hold
the one entry.
+If the feed cannot be copied, the exchange fails with a
`RuntimeCamelException` (caused by the
+`CloneNotSupportedException`) and the `SyndEntry` as the body, so the route's
error handler,
+such as a dead letter channel, receives the entry.
+
== Example
If the URL for the RSS feed uses query parameters, this component will
diff --git a/components/camel-rss/src/main/docs/rss-component.adoc
b/components/camel-rss/src/main/docs/rss-component.adoc
index 3d7fdeb1d784..b7fb1f079906 100644
--- a/components/camel-rss/src/main/docs/rss-component.adoc
+++ b/components/camel-rss/src/main/docs/rss-component.adoc
@@ -62,6 +62,11 @@ a `SyndFeed` with one `SyndEntry` or the whole `SyndFeed`
with all its `SyndEntr
|`splitEntries` |`false` |The entire list of entries from the current feed is
set in the exchange.
|=======================================================================
+With `splitEntries=true`, Camel copies the feed (with ROME's `clone`) to hold
the one entry.
+If the feed cannot be copied, the exchange fails with a
`RuntimeCamelException` (caused by the
+`CloneNotSupportedException`) and the `SyndEntry` as the body, so the route's
error handler,
+such as a dead letter channel, receives the entry.
+
== Example
If the URL for the RSS feed uses query parameters, this component will
diff --git
a/components/camel-rss/src/main/java/org/apache/camel/component/rss/RssEndpoint.java
b/components/camel-rss/src/main/java/org/apache/camel/component/rss/RssEndpoint.java
index 922204db4b5c..f3f90398e850 100644
---
a/components/camel-rss/src/main/java/org/apache/camel/component/rss/RssEndpoint.java
+++
b/components/camel-rss/src/main/java/org/apache/camel/component/rss/RssEndpoint.java
@@ -24,6 +24,7 @@ import org.apache.camel.Category;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.Producer;
+import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.feed.FeedComponent;
import org.apache.camel.component.feed.FeedEndpoint;
import org.apache.camel.component.feed.FeedPollingConsumer;
@@ -81,15 +82,16 @@ public class RssEndpoint extends FeedEndpoint implements
EndpointServiceLocation
@Override
public Exchange createExchange(Object feed, Object entry) {
Exchange exchange = createExchangeWithFeedHeader(feed,
RssConstants.RSS_FEED);
- SyndFeed newFeed;
try {
- newFeed = (SyndFeed) ((SyndFeed) feed).clone();
+ SyndFeed newFeed = (SyndFeed) ((SyndFeed) feed).clone();
newFeed.setEntries(Arrays.asList((SyndEntry) entry));
+ exchange.getIn().setBody(newFeed);
} catch (CloneNotSupportedException e) {
- LOG.debug("Could not create a new feed. This exception will be
ignored.", e);
- newFeed = null;
+ // keep the entry as the body so the error handler (such as a dead
letter channel) does not lose it
+ LOG.warn("Could not clone the feed to hold the entry. The exchange
is failed with the entry as its body.", e);
+ exchange.getIn().setBody(entry);
+ exchange.setException(new RuntimeCamelException("Could not clone
the feed to hold the entry", e));
}
- exchange.getIn().setBody(newFeed);
return exchange;
}
diff --git
a/components/camel-rss/src/test/java/org/apache/camel/component/rss/RssEntryCloneFailureTest.java
b/components/camel-rss/src/test/java/org/apache/camel/component/rss/RssEntryCloneFailureTest.java
new file mode 100644
index 000000000000..484ca53869ba
--- /dev/null
+++
b/components/camel-rss/src/test/java/org/apache/camel/component/rss/RssEntryCloneFailureTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.component.rss;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.rometools.rome.feed.synd.SyndEntry;
+import com.rometools.rome.feed.synd.SyndEntryImpl;
+import com.rometools.rome.feed.synd.SyndFeed;
+import com.rometools.rome.feed.synd.SyndFeedImpl;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.feed.FeedEndpoint;
+import org.apache.camel.component.feed.FeedPollingConsumer;
+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.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * When the feed cannot be cloned, the split consumer must fail the exchange
instead of sending a null body.
+ */
+public class RssEntryCloneFailureTest extends CamelTestSupport {
+
+ @Test
+ public void testCloneFailureFailsTheExchange() throws Exception {
+ MockEndpoint result = getMockEndpoint("mock:result");
+ result.expectedMessageCount(0);
+ MockEndpoint dead = getMockEndpoint("mock:dead");
+ dead.expectedMessageCount(2);
+
+ MockEndpoint.assertIsSatisfied(context);
+
+ for (Exchange exchange : dead.getReceivedExchanges()) {
+ Throwable caught = exchange.getProperty(Exchange.EXCEPTION_CAUGHT,
Throwable.class);
+ assertInstanceOf(RuntimeCamelException.class, caught);
+ assertInstanceOf(CloneNotSupportedException.class,
caught.getCause());
+ }
+ Set<String> titles = dead.getReceivedExchanges().stream()
+ .map(e -> e.getIn().getBody(SyndEntry.class).getTitle())
+ .collect(Collectors.toSet());
+ assertEquals(Set.of("Entry 1", "Entry 2"), titles);
+ }
+
+ @Test
+ public void testCreateExchangeKeepsEntryAndSetsException() {
+ RssEndpoint endpoint =
context.getEndpoint("rss:file:src/test/data/rss20.xml", RssEndpoint.class);
+ SyndFeed feed = createNonCloneableFeed();
+ SyndEntry entry = feed.getEntries().get(0);
+
+ Exchange exchange = endpoint.createExchange(feed, entry);
+
+ assertSame(entry, exchange.getIn().getBody());
+ assertInstanceOf(RuntimeCamelException.class, exchange.getException());
+ assertInstanceOf(CloneNotSupportedException.class,
exchange.getException().getCause());
+ assertSame(feed, exchange.getIn().getHeader(RssConstants.RSS_FEED));
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ public void configure() throws Exception {
+ errorHandler(deadLetterChannel("mock:dead"));
+
+ RssEndpoint rss = new NonCloneableFeedEndpoint();
+ rss.setCamelContext(context);
+ rss.setFeedUri("file:src/test/data/rss20.xml");
+ rss.setSplitEntries(true);
+ rss.setDelay(60000);
+ rss.setThrottleEntries(false);
+ context.addEndpoint("myrss", rss);
+
+ from("myrss").to("mock:result");
+ }
+ };
+ }
+
+ private static SyndFeed createNonCloneableFeed() {
+ SyndFeed feed = new NonCloneableFeed();
+ feed.setFeedType("rss_2.0");
+ feed.setTitle("Feed");
+ List<SyndEntry> entries = new ArrayList<>();
+ for (int i = 1; i <= 2; i++) {
+ SyndEntry entry = new SyndEntryImpl();
+ entry.setTitle("Entry " + i);
+ entries.add(entry);
+ }
+ feed.setEntries(entries);
+ return feed;
+ }
+
+ private static class NonCloneableFeed extends SyndFeedImpl {
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ throw new CloneNotSupportedException("Cannot clone this feed");
+ }
+ }
+
+ private static class NonCloneableFeedEndpoint extends RssEndpoint {
+ @Override
+ protected FeedPollingConsumer createEntryPollingConsumer(
+ FeedEndpoint feedEndpoint, Processor processor, boolean
throttleEntries)
+ throws Exception {
+ RssEntryPollingConsumer answer = new RssEntryPollingConsumer(this,
processor, throttleEntries) {
+ @Override
+ protected Object createFeed() {
+ return createNonCloneableFeed();
+ }
+ };
+ configureConsumer(answer);
+ return answer;
+ }
+ }
+}