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 6f27bbc889c8 CAMEL-26824: camel-core - an Error of an expression, a
data type transformer or the producer of an API component fails the exchange
(#29665)
6f27bbc889c8 is described below
commit 6f27bbc889c8a6ec1b6e4713ee86c573ae3050a4
Author: allthingssecurity <[email protected]>
AuthorDate: Thu Nov 12 17:33:22 2026 +0530
CAMEL-26824: camel-core - an Error of an expression, a data type
transformer or the producer of an API component fails the exchange (#29665)
* CAMEL-26824: camel-core - an Error of an expression, a data type
transformer or the producer of an API component fails the exchange
Follow-up of CAMEL-26741, which made the processor adapters fail the
exchange with an Error (other than a VirtualMachineError) instead of
calling the callback and throwing the Error on. These processors had the
same pattern (catch Exception, callback in finally), so an Error of the
code they call let the route continue as if the step had succeeded:
- EvaluateExpressionProcessor (the expression of a recipientList);
- MulticastProcessor: the synchronous branch (split, multicast and
recipientList with synchronous=true), and doProcess around the
creation of the parts, which now goes through doDone as for an
exception (in the default mode the Error already failed the
exchange, but skipped doDone);
- DataTypeProcessor and DelegatingDataTypeProcessor (data type
transformers);
- AbstractApiProducer (the invocation of a component built on the API
component framework, on a thread of the component: an Error outside
the reflective call of the API method, such as when the client is
created, was lost in the Future of the executor).
They now set such an Error on the exchange, and leave a
VirtualMachineError as before (thrown on after the callback), as the
adapters do. DelegatingDataTypeProcessor called the callback twice when
the data type property was missing; it is now called once. The 4.24
upgrade guide entry of CAMEL-26741 and the error handler page name them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* CAMEL-26824: camel-core - a test for an Error of the aggregation strategy
of a synchronous split, and the VirtualMachineError of an API producer is logged
Review of #29665:
- The catch around the await manager in the synchronous branch of
MulticastProcessor.process is reached by an Error of the
AggregationStrategy
(MulticastTask and the reactive executor catch only exceptions): new test
synchronousSplitFailsTheExchangeWithAnErrorOfItsAggregationStrategy (fails
with that catch back at Exception). The upgrade guide and
error-handler.adoc
name it (synchronous=true without parallelProcessing).
- AbstractApiProducer: a VirtualMachineError on the executor thread is
thrown on
as before, but the Future of submit() drops it, so it is logged at ERROR
first.
- error-handler.adoc also names the contract transformers and validators and
the transacted step of #29668 (CAMEL-26866), which is merged.
- Tests: assertInstanceOf, the processors started in a test are stopped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../processor/EvaluateExpressionProcessor.java | 7 +-
.../apache/camel/processor/MulticastProcessor.java | 13 +-
.../processor/transformer/DataTypeProcessor.java | 7 +-
.../transformer/DelegatingDataTypeProcessor.java | 9 +-
.../processor/SiblingProcessorsThrowErrorTest.java | 279 +++++++++++++++++++++
.../support/component/AbstractApiProducerTest.java | 40 +++
.../support/component/AbstractApiProducer.java | 12 +
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 9 +
.../modules/ROOT/pages/error-handler.adoc | 6 +-
9 files changed, 375 insertions(+), 7 deletions(-)
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/EvaluateExpressionProcessor.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/EvaluateExpressionProcessor.java
index 416254c020b6..7d1eb00676e6 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/EvaluateExpressionProcessor.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/EvaluateExpressionProcessor.java
@@ -41,7 +41,12 @@ public class EvaluateExpressionProcessor extends
BaseProcessorSupport implements
try {
Object result = expression.evaluate(exchange, Object.class);
exchange.setProperty(ExchangePropertyKey.EVALUATE_EXPRESSION_RESULT, result);
- } catch (Exception e) {
+ } catch (VirtualMachineError e) {
+ // left as before (thrown on after the callback), as in
DelegateSyncProcessor
+ throw e;
+ } catch (Throwable e) {
+ // also an Error of the code it calls: the callback is called in
finally, so an Error thrown on after it
+ // would not fail the exchange
exchange.setException(e);
} finally {
callback.done(true);
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java
index 78eb0db6132e..8d01c21f294c 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java
@@ -351,7 +351,12 @@ public class MulticastProcessor extends
BaseProcessorSupport
return doProcess(exchange, callback);
}
}, exchange);
- } catch (Exception e) {
+ } catch (VirtualMachineError e) {
+ // left as before (thrown on after the callback), as in
DelegateSyncProcessor
+ throw e;
+ } catch (Throwable e) {
+ // also an Error (such as one of an expression): the callback
is called in finally, so an Error thrown
+ // on after it would not fail the exchange
exchange.setException(e);
} finally {
callback.done(true);
@@ -372,7 +377,11 @@ public class MulticastProcessor extends
BaseProcessorSupport
.stream().filter(Objects::nonNull).toList();
size = ((Collection<ProcessorExchangePair>) pairs).size();
}
- } catch (Exception e) {
+ } catch (VirtualMachineError e) {
+ // left as before (thrown on), as in DelegateSyncProcessor
+ throw e;
+ } catch (Throwable e) {
+ // also an Error, such as one of the split expression
exchange.setException(e);
// unexpected exception was thrown, maybe from iterator etc. so do
not regard as exhausted
// and do the done work
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/transformer/DataTypeProcessor.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/transformer/DataTypeProcessor.java
index f14dbfc7dba1..1a223c4960bb 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/transformer/DataTypeProcessor.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/transformer/DataTypeProcessor.java
@@ -95,7 +95,12 @@ public class DataTypeProcessor extends BaseProcessorSupport
implements CamelCont
if (target != null) {
target.transform(message, fromDT, toDataType);
}
- } catch (Exception e) {
+ } catch (VirtualMachineError e) {
+ // left as before (thrown on after the callback), as in
DelegateSyncProcessor
+ throw e;
+ } catch (Throwable e) {
+ // also an Error of the code it calls: the callback is called in
finally, so an Error thrown on after it
+ // would not fail the exchange
exchange.setException(e);
} finally {
callback.done(true);
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/transformer/DelegatingDataTypeProcessor.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/transformer/DelegatingDataTypeProcessor.java
index d43d6de970a2..84f4dbcf7f75 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/transformer/DelegatingDataTypeProcessor.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/transformer/DelegatingDataTypeProcessor.java
@@ -62,7 +62,7 @@ public class DelegatingDataTypeProcessor extends
BaseProcessorSupport implements
exchange.setException(new IllegalArgumentException(
"Exchange property '" + CAMEL_DATA_TYPE_PROPERTY + "'
is not set. "
+ "Cannot
determine target data type for transformation."));
- callback.done(true);
+ // the callback is called in finally
return true;
}
@@ -84,7 +84,12 @@ public class DelegatingDataTypeProcessor extends
BaseProcessorSupport implements
} else {
transformer.transform(message, fromDataType, toDataType);
}
- } catch (Exception e) {
+ } catch (VirtualMachineError e) {
+ // left as before (thrown on after the callback), as in
DelegateSyncProcessor
+ throw e;
+ } catch (Throwable e) {
+ // also an Error of the code it calls: the callback is called in
finally, so an Error thrown on after it
+ // would not fail the exchange
exchange.setException(e);
} finally {
callback.done(true);
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/SiblingProcessorsThrowErrorTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/SiblingProcessorsThrowErrorTest.java
new file mode 100644
index 000000000000..320ca60aa958
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/SiblingProcessorsThrowErrorTest.java
@@ -0,0 +1,279 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+
+import org.apache.camel.AsyncProcessor;
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePropertyKey;
+import org.apache.camel.Expression;
+import org.apache.camel.Message;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.processor.transformer.DataTypeProcessor;
+import org.apache.camel.processor.transformer.DelegatingDataTypeProcessor;
+import org.apache.camel.spi.DataType;
+import org.apache.camel.spi.Transformer;
+import org.apache.camel.spi.TransformerKey;
+import org.apache.camel.support.service.ServiceHelper;
+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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The processors of camel-core that call the callback in a finally block fail
the exchange with an Error of the code
+ * they call, as the processor adapters do (CAMEL-26741), instead of calling
the callback and throwing the Error on: the
+ * route would continue as if the step succeeded. A {@link
VirtualMachineError} is left as before (thrown on after the
+ * callback).
+ */
+class SiblingProcessorsThrowErrorTest extends ContextTestSupport {
+
+ private static final AssertionError ERROR = new AssertionError("Simulated
error of user code");
+ private static final StackOverflowError SOE = new
StackOverflowError("test");
+
+ @Test
+ void evaluateExpressionProcessorFailsTheExchangeWithAnError() {
+ assertCallbackCalledOnceWithTheError(new
EvaluateExpressionProcessor(throwingExpression(ERROR)));
+ }
+
+ @Test
+ void
evaluateExpressionProcessorThrowsAVirtualMachineErrorOnAfterTheCallback() {
+ assertVirtualMachineErrorThrownOnAfterTheCallback(new
EvaluateExpressionProcessor(throwingExpression(SOE)));
+ }
+
+ @Test
+ void dataTypeProcessorFailsTheExchangeWithAnError() throws Exception {
+ DataTypeProcessor processor = dataTypeProcessor(ERROR);
+ try {
+ assertCallbackCalledOnceWithTheError(processor);
+ } finally {
+ ServiceHelper.stopService(processor);
+ }
+ }
+
+ @Test
+ void dataTypeProcessorThrowsAVirtualMachineErrorOnAfterTheCallback()
throws Exception {
+ DataTypeProcessor processor = dataTypeProcessor(SOE);
+ try {
+ assertVirtualMachineErrorThrownOnAfterTheCallback(processor);
+ } finally {
+ ServiceHelper.stopService(processor);
+ }
+ }
+
+ @Test
+ void delegatingDataTypeProcessorFailsTheExchangeWithAnError() throws
Exception {
+ context.getTransformerRegistry().put(new TransformerKey(DataType.ANY,
new DataType("failing")),
+ new ThrowingTransformer(ERROR));
+ DelegatingDataTypeProcessor processor = new
DelegatingDataTypeProcessor();
+ processor.setCamelContext(context);
+ processor.start();
+ try {
+ Exchange exchange = createExchangeWithBody("Hello");
+ exchange.setProperty("CamelDataType", "failing");
+ List<Boolean> calls = new ArrayList<>();
+
+ assertTrue(processor.process(exchange, calls::add));
+
+ assertEquals(List.of(true), calls);
+ assertFailedWithTheError(exchange.getException());
+ } finally {
+ ServiceHelper.stopService(processor);
+ }
+ }
+
+ @Test
+ void
delegatingDataTypeProcessorCallsTheCallbackOnceWithoutTheDataTypeProperty()
throws Exception {
+ DelegatingDataTypeProcessor processor = new
DelegatingDataTypeProcessor();
+ processor.setCamelContext(context);
+ processor.start();
+ try {
+ Exchange exchange = createExchangeWithBody("Hello");
+ List<Boolean> calls = new ArrayList<>();
+
+ assertTrue(processor.process(exchange, calls::add));
+
+ assertEquals(List.of(true), calls, "The callback must be called
once");
+ assertInstanceOf(IllegalArgumentException.class,
exchange.getException());
+ } finally {
+ ServiceHelper.stopService(processor);
+ }
+ }
+
+ @Test
+ void synchronousSplitFailsTheExchangeWithAnErrorOfItsExpression() throws
Exception {
+ getMockEndpoint("mock:after").expectedMessageCount(0);
+ getMockEndpoint("mock:handled").expectedMessageCount(1);
+
+ getMockEndpoint("mock:part").expectedMessageCount(0);
+
+ Exchange out = template.send("direct:split", e ->
e.getMessage().setBody("a,b"));
+
+ assertMockEndpointsSatisfied();
+ assertNull(out.getException(), "onException must handle the Error");
+ // the split completed its aggregation strategy (doDone), as for an
exception
+ assertNull(out.getProperty(ExchangePropertyKey.AGGREGATION_STRATEGY));
+ Throwable caught = out.getProperty(Exchange.EXCEPTION_CAUGHT,
Throwable.class);
+ assertSame(ERROR, caught instanceof CamelExecutionException ?
caught.getCause() : caught);
+ }
+
+ @Test
+ void synchronousSplitFailsTheExchangeWithAnErrorOfItsAggregationStrategy()
throws Exception {
+ // the Error leaves the split task (which catches only exceptions)
through the reactive executor and the await
+ // manager of the synchronous split, and reaches the catch around the
await manager: the exchange fails instead
+ // of continuing after the callback (doDone of the split is skipped on
this path, as before)
+ getMockEndpoint("mock:part").expectedBodiesReceived("a");
+ getMockEndpoint("mock:after").expectedMessageCount(0);
+ getMockEndpoint("mock:handled").expectedMessageCount(1);
+
+ Exchange out = template.send("direct:splitAggregate", e ->
e.getMessage().setBody("a,b"));
+
+ assertMockEndpointsSatisfied();
+ assertNull(out.getException(), "onException must handle the Error");
+ Throwable caught = out.getProperty(Exchange.EXCEPTION_CAUGHT,
Throwable.class);
+ assertSame(ERROR, caught instanceof CamelExecutionException ?
caught.getCause() : caught);
+ }
+
+ @Test
+ void splitFailsTheExchangeWithAnErrorOfItsExpression() throws Exception {
+ // the default (not synchronous) split: the Error failed the exchange
through the engine before too, but skipped
+ // the completion of the split
+ getMockEndpoint("mock:part").expectedMessageCount(0);
+ getMockEndpoint("mock:after").expectedMessageCount(0);
+ getMockEndpoint("mock:handled").expectedMessageCount(1);
+
+ Exchange out = template.send("direct:asyncSplit", e ->
e.getMessage().setBody("a,b"));
+
+ assertMockEndpointsSatisfied();
+ assertNull(out.getException(), "onException must handle the Error");
+ // the split completed its aggregation strategy (doDone), as for an
exception
+ assertNull(out.getProperty(ExchangePropertyKey.AGGREGATION_STRATEGY));
+ }
+
+ @Test
+ void recipientListFailsTheExchangeWithAnErrorOfItsExpression() throws
Exception {
+ getMockEndpoint("mock:after").expectedMessageCount(0);
+ getMockEndpoint("mock:handled").expectedMessageCount(1);
+
+ Exchange out = template.send("direct:recipientList", e ->
e.getMessage().setBody("a"));
+
+ assertMockEndpointsSatisfied();
+ assertNull(out.getException(), "onException must handle the Error");
+ }
+
+ private DataTypeProcessor dataTypeProcessor(Error error) throws Exception {
+ context.getTransformerRegistry().put(new TransformerKey(DataType.ANY,
new DataType("failing")),
+ new ThrowingTransformer(error));
+ DataTypeProcessor processor = new DataTypeProcessor(null, "failing");
+ processor.setCamelContext(context);
+ processor.start();
+ return processor;
+ }
+
+ private void assertCallbackCalledOnceWithTheError(AsyncProcessor
processor) {
+ Exchange exchange = createExchangeWithBody("Hello");
+ List<Boolean> calls = new ArrayList<>();
+
+ assertTrue(processor.process(exchange, calls::add));
+
+ assertEquals(List.of(true), calls);
+ assertFailedWithTheError(exchange.getException());
+ }
+
+ private void
assertVirtualMachineErrorThrownOnAfterTheCallback(AsyncProcessor processor) {
+ Exchange exchange = createExchangeWithBody("Hello");
+ List<Boolean> calls = new ArrayList<>();
+
+ StackOverflowError thrown = assertThrows(StackOverflowError.class, ()
-> processor.process(exchange, calls::add));
+
+ assertSame(SOE, thrown);
+ assertEquals(List.of(true), calls);
+ // the current behaviour of a VirtualMachineError (as in
ProcessorThrowsErrorTest), not a contract
+ assertNull(exchange.getException());
+ }
+
+ private static void assertFailedWithTheError(Exception exception) {
+ // the exchange keeps an Error wrapped in a CamelExecutionException
+ assertSame(ERROR, assertInstanceOf(CamelExecutionException.class,
exception).getCause());
+ }
+
+ private static Expression throwingExpression(Error error) {
+ return new Expression() {
+ @Override
+ public <T> T evaluate(Exchange exchange, Class<T> type) {
+ throw error;
+ }
+ };
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:split")
+
.onException(AssertionError.class).handled(true).to("mock:handled").end()
+ .split(throwingExpression(ERROR)).synchronous()
+ .to("mock:part")
+ .end()
+ .to("mock:after");
+
+ from("direct:splitAggregate")
+
.onException(AssertionError.class).handled(true).to("mock:handled").end()
+ .split(body().tokenize(","), (oldExchange,
newExchange) -> {
+ throw ERROR;
+ }).synchronous()
+ .to("mock:part")
+ .end()
+ .to("mock:after");
+
+ from("direct:asyncSplit")
+
.onException(AssertionError.class).handled(true).to("mock:handled").end()
+ .split(throwingExpression(ERROR))
+ .to("mock:part")
+ .end()
+ .to("mock:after");
+
+ from("direct:recipientList")
+
.onException(AssertionError.class).handled(true).to("mock:handled").end()
+ .recipientList(throwingExpression(ERROR))
+ .to("mock:after");
+ }
+ };
+ }
+
+ private static final class ThrowingTransformer extends Transformer {
+ private final Error error;
+
+ ThrowingTransformer(Error error) {
+ this.error = error;
+ }
+
+ @Override
+ public void transform(Message message, DataType from, DataType to) {
+ throw error;
+ }
+ }
+}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/support/component/AbstractApiProducerTest.java
b/core/camel-core/src/test/java/org/apache/camel/support/component/AbstractApiProducerTest.java
index 6312a0bdc7fe..22cb921d71fc 100644
---
a/core/camel-core/src/test/java/org/apache/camel/support/component/AbstractApiProducerTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/support/component/AbstractApiProducerTest.java
@@ -21,6 +21,7 @@ import java.util.List;
import java.util.Map;
import org.apache.camel.CamelContext;
+import org.apache.camel.CamelExecutionException;
import org.apache.camel.Component;
import org.apache.camel.Consumer;
import org.apache.camel.ContextTestSupport;
@@ -36,6 +37,9 @@ import
org.apache.camel.support.component.ApiMethodHelperTest.TestMethod;
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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -89,6 +93,32 @@ public class AbstractApiProducerTest extends
ContextTestSupport {
assertEquals(2, ((String[]) out).length);
}
+ @Test
+ public void testErrorOutsideTheReflectiveCallFailsTheExchange() throws
Exception {
+ TestApiEndpoint endpoint = (TestApiEndpoint) createEndpoint("greetMe",
"name");
+ AssertionError error = new AssertionError("Simulated error of the
API");
+ endpoint.failWith = error;
+
+ // thrown by doInvokeMethod itself, outside the reflective call of the
API method (which wraps an Error of the
+ // method), as a producer that calls its client directly can do. It
runs on a thread of the endpoint: the Error
+ // must fail the exchange, not be lost in that thread
+ Exchange out = template.send(endpoint, e ->
e.getMessage().setBody("World"));
+
+ assertSame(error, assertInstanceOf(CamelExecutionException.class,
out.getException()).getCause());
+ }
+
+ @Test
+ public void testVirtualMachineErrorCompletesTheExchangeAsBefore() throws
Exception {
+ TestApiEndpoint endpoint = (TestApiEndpoint) createEndpoint("greetMe",
"name");
+ endpoint.failWith = new StackOverflowError("test");
+
+ // left as before: the callback completes the exchange, and the error
is thrown on to the executor thread. The
+ // current behaviour, not a contract
+ Exchange out = template.send(endpoint, e ->
e.getMessage().setBody("World"));
+
+ assertNull(out.getException());
+ }
+
record TestName(String name) {
}
@@ -180,6 +210,8 @@ public class AbstractApiProducerTest extends
ContextTestSupport {
static class TestApiEndpoint extends AbstractApiEndpoint<TestApiName,
TestConfiguration> {
private final ApiMethodPropertiesHelper<TestConfiguration>
propertiesHelper;
+ // when set, the API method throws it
+ volatile Error failWith;
TestApiEndpoint(String uri, Component component, String methodName) {
super(uri, component, TestApiName.TEST, methodName, METHOD_HELPER,
new TestEndpointConfiguration());
@@ -211,6 +243,14 @@ public class AbstractApiProducerTest extends
ContextTestSupport {
@Override
public Producer createProducer() {
return new AbstractApiProducer<>(this, propertiesHelper) {
+ @Override
+ protected Object doInvokeMethod(ApiMethod method, Map<String,
Object> properties) {
+ Error error = failWith;
+ if (error != null) {
+ throw error;
+ }
+ return super.doInvokeMethod(method, properties);
+ }
};
}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiProducer.java
b/core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiProducer.java
index a3c1884e35d6..7060c3dc6431 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiProducer.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiProducer.java
@@ -96,6 +96,18 @@ public abstract class AbstractApiProducer<E extends Enum<E>
& ApiName, T>
} catch (Exception t) {
exchange.setException(RuntimeCamelException.wrapRuntimeCamelException(t));
+ } catch (VirtualMachineError e) {
+ // left as before (thrown on after the callback), as in
DelegateSyncProcessor. On this thread of the
+ // executor it is kept in the Future that nobody reads, so
log it to make it visible at least
+ log.error("Error invoking operation {} on exchange {}: the
exchange completes as if the call succeeded,"
+ + " and the error is thrown on to the executor
thread, which drops it",
+ method.getName(), exchange.getExchangeId(), e);
+ throw e;
+ } catch (Throwable e) {
+ // also an Error outside the reflective call of the API
method (which wraps an Error of the method),
+ // such as when the client is created: the callback is
called in finally, and an Error thrown on
+ // after it would not fail the exchange, and would be lost
in the Future of the executor
+ exchange.setException(e);
} finally {
callback.done(false);
}
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 6b61780d9d37..d8a069d1ace4 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
@@ -766,6 +766,15 @@ exception and do not throw it. `sendBody(...)` and
`requestBody(...)` throw a `C
must now check the exception of the returned exchange
(`exchange.getException()`), or send with `sendBody(...)` or
`requestBody(...)`.
+The same now applies to an `Error` that is not a `VirtualMachineError` thrown
by the expression of a `recipientList`, by
+the expression of a `split` with `synchronous=true` (or while a `multicast` or
`recipientList` with `synchronous=true`
+prepares its parts), by the `AggregationStrategy` of a `split`, `multicast` or
`recipientList` with `synchronous=true`
+and without `parallelProcessing`, by a data type transformer (`transform` to a
data type), and by the producer of a
+component built on the API component framework when it is not thrown by an API
method called by reflection (for example
+when the producer creates its client, or calls a client method directly); an
`Error` of an API method called by
+reflection already failed the exchange. Before, depending on the error
handler, the route continued as if the step had
+succeeded, or the `Error` reached the caller (for the producer of an API
component, the `Error` was lost on its thread).
+
There is no option for the old behaviour. See also
xref:error-handler.adoc#_an_error_thrown_by_a_processor[An Error thrown by a
processor].
diff --git a/docs/user-manual/modules/ROOT/pages/error-handler.adoc
b/docs/user-manual/modules/ROOT/pages/error-handler.adoc
index b35127b3006e..99fb7f165360 100644
--- a/docs/user-manual/modules/ROOT/pages/error-handler.adoc
+++ b/docs/user-manual/modules/ROOT/pages/error-handler.adoc
@@ -185,7 +185,11 @@ additional documentation of the features above.
== An Error thrown by a processor
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:
+`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):
[cols="2,4",options="header"]
|===