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 7acfd1c80a27 CAMEL-26888: camel-core - an Error of the aggregation
strategy of an enrich, or of a split watermark, fails the exchange (#29792)
7acfd1c80a27 is described below
commit 7acfd1c80a27550c4bcccae6a0856a28b01275d0
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Nov 13 20:23:47 2026 +0530
CAMEL-26888: camel-core - an Error of the aggregation strategy of an
enrich, or of a split watermark, fails the exchange (#29792)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../java/org/apache/camel/processor/Enricher.java | 8 +-
.../org/apache/camel/processor/PollEnricher.java | 8 +-
.../java/org/apache/camel/processor/Splitter.java | 8 +-
.../SplitterWatermarkThrowsErrorTest.java | 86 +++++++++++++++++++
...EnricherAggregationStrategyThrowsErrorTest.java | 98 ++++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 7 +-
.../modules/ROOT/pages/error-handler.adoc | 14 +++-
7 files changed, 218 insertions(+), 11 deletions(-)
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/Enricher.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/Enricher.java
index de329376788d..b4a9e53cf338 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/Enricher.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/Enricher.java
@@ -254,8 +254,12 @@ public class Enricher extends BaseProcessorSupport
copyResultsWithoutCorrelationId(exchange,
aggregatedExchange);
}
used = true;
- } catch (Exception e) {
- // if the aggregationStrategy threw an exception, set
it on the original exchange
+ } catch (VirtualMachineError e) {
+ // left as before: the application must not handle a
JVM-level failure with Camel's error handling
+ throw e;
+ } catch (Throwable e) {
+ // if the aggregationStrategy threw an exception (also
an Error, which would escape this callback),
+ // set it on the original exchange
exchange.setException(new
CamelExchangeException("Error occurred during aggregation", exchange, e));
if (resourceExchange.getException() == null) {
resourceExchange.setException(e);
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/PollEnricher.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/PollEnricher.java
index 27f21ba74637..0df05c39cb16 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/PollEnricher.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/PollEnricher.java
@@ -470,7 +470,11 @@ public class PollEnricher extends BaseProcessorSupport
implements IdAware, Route
// set property with the uri of the endpoint enriched so we can
use that for tracing etc
exchange.setProperty(ExchangePropertyKey.TO_ENDPOINT,
consumer.getEndpoint().getEndpointUri());
- } catch (Exception e) {
+ } catch (VirtualMachineError e) {
+ // left as before: the application must not handle a JVM-level
failure with Camel's error handling
+ throw e;
+ } catch (Throwable e) {
+ // also an Error of the aggregation strategy (such as a
NoClassDefFoundError), as for an exception
exchange.setException(new CamelExchangeException("Error occurred
during aggregation", exchange, e));
// the resource was not aggregated, so release it as failed now
(such as rolling back a polled file),
// and not together with the exchange, which may still complete
successfully (such as by redelivery)
@@ -489,7 +493,7 @@ public class PollEnricher extends BaseProcessorSupport
implements IdAware, Route
}
}
- private static void doneResourceAsFailed(Exchange resourceExchange,
Exception cause) {
+ private static void doneResourceAsFailed(Exchange resourceExchange,
Throwable cause) {
if (resourceExchange != null) {
if (cause != null && resourceExchange.getException() == null) {
resourceExchange.setException(cause);
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/Splitter.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/Splitter.java
index 0e2fb025bd47..fd8fd03733b7 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/Splitter.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/Splitter.java
@@ -737,8 +737,12 @@ public class Splitter extends MulticastProcessor {
String value;
try {
value = watermarkExpression.evaluate(subExchange,
String.class);
- } catch (Exception e) {
- // fail the item, as an exception thrown here would make
the split exchange hang (never complete)
+ } catch (VirtualMachineError e) {
+ // left as before: the application must not handle a
JVM-level failure with Camel's error handling
+ throw e;
+ } catch (Throwable e) {
+ // fail the item, as an exception (or an Error such as a
NoClassDefFoundError) thrown here would
+ // make the split exchange hang (never complete)
subExchange.setException(e);
return;
}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/SplitterWatermarkThrowsErrorTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/SplitterWatermarkThrowsErrorTest.java
new file mode 100644
index 000000000000..9b2b9f68c441
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/SplitterWatermarkThrowsErrorTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.processor;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.SimpleFunction;
+import org.apache.camel.support.PluginHelper;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * The watermark expression of a split throws an Error (not a
VirtualMachineError): the split completes and fails, as
+ * for an exception (CAMEL-26710).
+ */
+class SplitterWatermarkThrowsErrorTest extends ContextTestSupport {
+
+ private final Map<String, String> store = new ConcurrentHashMap<>();
+
+ @Test
+ void testSplitCompletesWhenTheWatermarkExpressionThrowsAnError() throws
Exception {
+ getMockEndpoint("mock:split").expectedMessageCount(2);
+
+ Exchange out = template.asyncSend("direct:start", e ->
e.getIn().setBody(Arrays.asList("a", "b")))
+ .get(5, TimeUnit.SECONDS);
+
+ MockEndpoint.assertIsSatisfied(context);
+ // the item failed with the Error (which the exchange keeps wrapped,
as for any Error)
+ assertNotNull(out.getException());
+ assertInstanceOf(NoClassDefFoundError.class,
out.getException().getCause());
+ assertFalse(store.containsKey("errorJob"));
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() ->
assertEquals(0, context.getInflightRepository().size()));
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ // a function of the watermark expression that needs a class
that is missing
+
PluginHelper.getSimpleFunctionRegistry(context).addFunction(new
SimpleFunction() {
+ @Override
+ public String getName() {
+ return "boom";
+ }
+
+ @Override
+ public Object apply(Exchange exchange, Object input) {
+ throw new NoClassDefFoundError("Simulated missing
class");
+ }
+ });
+
+ from("direct:start")
+ .split(body()).resumeStrategy(new
SplitterTestResumeStrategy(store), "errorJob")
+ .watermarkExpression("${function(boom)}")
+ .to("mock:split");
+ }
+ };
+ }
+}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/enricher/EnricherAggregationStrategyThrowsErrorTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/enricher/EnricherAggregationStrategyThrowsErrorTest.java
new file mode 100644
index 000000000000..1337d366fdb7
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/enricher/EnricherAggregationStrategyThrowsErrorTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.processor.enricher;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.AggregationStrategy;
+import org.apache.camel.CamelExchangeException;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * The aggregation strategy of an enrich or a pollEnrich throws an Error (not
a VirtualMachineError): the exchange fails
+ * as for an exception of the strategy, and does not stay inflight.
+ */
+class EnricherAggregationStrategyThrowsErrorTest extends ContextTestSupport {
+
+ private final AggregationStrategy strategy = (oldExchange, newExchange) ->
{
+ throw new NoClassDefFoundError("Simulated missing class");
+ };
+ private final AtomicInteger attempts = new AtomicInteger();
+
+ @Test
+ void testEnrich() throws Exception {
+
+ Exchange out = template.asyncSend("direct:enrich", e ->
e.getIn().setBody("Hello")).get(5, TimeUnit.SECONDS);
+
+ assertInstanceOf(CamelExchangeException.class, out.getException());
+ assertInstanceOf(NoClassDefFoundError.class,
out.getException().getCause());
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() ->
assertEquals(0, context.getInflightRepository().size()));
+ }
+
+ @Test
+ void testPollEnrichReleasesTheFileAfterAnError() {
+ template.sendBodyAndHeader(fileUri("data"), "Big file",
Exchange.FILE_NAME, "AAA.dat");
+
+ // the first aggregation fails with an Error
+ Exchange out = template.send("direct:pollFile", e ->
e.getIn().setBody("Start"));
+ assertNotNull(out.getException());
+
+ // the file must be released, so the second poll gets it again
+ assertEquals("Big file", template.requestBody("direct:pollFile",
"Start", String.class));
+ }
+
+ @Test
+ void testPollEnrich() throws Exception {
+ template.sendBody("seda:resource", "World");
+
+ Exchange out = template.asyncSend("direct:pollEnrich", e ->
e.getIn().setBody("Hello")).get(5, TimeUnit.SECONDS);
+
+ assertInstanceOf(CamelExchangeException.class, out.getException());
+ assertInstanceOf(NoClassDefFoundError.class,
out.getException().getCause());
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() ->
assertEquals(0, context.getInflightRepository().size()));
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:enrich").enrich("direct:resource",
strategy).to("mock:result");
+ from("direct:resource").transform(constant("World"));
+
+
from("direct:pollFile").pollEnrich(fileUri("data?initialDelay=0&delay=10"),
2000, (original, resource) -> {
+ if (attempts.incrementAndGet() == 1) {
+ throw new NoClassDefFoundError("Simulated missing
class");
+ }
+
original.getMessage().setBody(resource.getMessage().getBody(String.class));
+ return original;
+ });
+
+ from("direct:pollEnrich").pollEnrich("seda:resource", 5000,
strategy).to("mock:result");
+ }
+ };
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index c50fc5208253..6e3583d77b8d 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -896,7 +896,12 @@ exchange as an exception does (wrapped in a
`CamelExecutionException`), also whe
* by a transformer or validator of the input or output type of a route
(`inputType`, `outputType`). Before, it was
thrown to the caller of the route without the end of the unit of work, so
the exchange could stay inflight;
* by a plain `Processor` of a transacted exchange in a step without an error
handler of its own (such as in a `doTry`,
- `doCatch` or `doFinally` block). Before, a `doCatch` did not get it, and it
was thrown to the caller.
+ `doCatch` or `doFinally` block). Before, a `doCatch` did not get it, and it
was thrown to the caller;
+* by the `AggregationStrategy` of an `enrich` or `pollEnrich` (as a
`CamelExchangeException` caused by it, as for an
+ exception of the strategy), and the resource is released as failed. Before,
for `enrich` the `Error` was thrown to the
+ caller and the exchange stayed inflight; for `pollEnrich` the resource (such
as a polled file) was not released;
+* by the watermark expression of a `split`: it fails that item, as an
exception does. Before, it was thrown to the
+ caller and the split exchange stayed inflight.
A `VirtualMachineError` is left as before. See the next section.
diff --git a/docs/user-manual/modules/ROOT/pages/error-handler.adoc
b/docs/user-manual/modules/ROOT/pages/error-handler.adoc
index 99fb7f165360..63592b9d8e27 100644
--- a/docs/user-manual/modules/ROOT/pages/error-handler.adoc
+++ b/docs/user-manual/modules/ROOT/pages/error-handler.adoc
@@ -186,10 +186,12 @@ additional documentation of the features above.
A plain `Processor` (in `process(...)`, in `bean(...)` for a bean that is a
`Processor`, or a producer that is not an
`AsyncProcessor`) may throw an `Error` instead of an exception. The same rules
apply to an `Error` of the expression of
-a `recipientList`, of the expression of a `split` with `synchronous=true`, of
the `AggregationStrategy` of a `split`,
-`multicast` or `recipientList` with `synchronous=true` and without
`parallelProcessing`, of a data type transformer, of
-a transformer or validator of the input or output type of a route
(`inputType`, `outputType`), and of a plain
-`Processor` of a transacted exchange in a step without an error handler of its
own (such as in a `doTry` block):
+a `recipientList`, of the expression of a `split` with `synchronous=true`, of
the watermark expression of a `split`
+(it fails that item), of the `AggregationStrategy` of a `split`, `multicast`
or `recipientList` with
+`synchronous=true` and without `parallelProcessing`, of the
`AggregationStrategy` of an `enrich` or `pollEnrich`, of a
+data type transformer, of a transformer or validator of the input or output
type of a route (`inputType`,
+`outputType`), and of a plain `Processor` of a transacted exchange in a step
without an error handler of its own (such
+as in a `doTry` block):
[cols="2,4",options="header"]
|===
@@ -202,6 +204,10 @@ failure: redelivery, `onException`, `doCatch` and a dead
letter channel get it.
`doCatch(Exception.class)` also match it; to treat it differently, match it on
its class, such as
`onException(AssertionError.class)`
+For the `AggregationStrategy` of an `enrich` or `pollEnrich`, the exchange
fails with a `CamelExchangeException`
+caused by the `Error` (not wrapped in a `CamelExecutionException`), as for an
exception of the strategy, and the
+resource is released as failed
+
|A `VirtualMachineError`, such as `OutOfMemoryError`, `StackOverflowError` or
`InternalError`
|Camel's error handling does not get it (the JVM may no longer be in a
consistent state, so the application should not
handle it). It is not set on the exchange, but thrown on after the processor's