This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-24513 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 92ee348526c13029c1aa6bfc6b50df36a4b5b158 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Aug 27 07:06:24 2026 +0200 CAMEL-24513: camel-core - Error registry for handled exception should point to origin of error DefaultErrorRegistry captured the failing node id/location by reading the last message history entry, which is only correct if nothing runs after the failure. For a handled exception, the exception handler itself (onException, doCatch, dead letter channel) can append its own message history entries before the registry looks, so toNode/location ended up pointing at a node touched by the handler instead of the node that actually failed. Add Exchange.FAILURE_NODE_ID / Exchange.FAILURE_LOCATION, captured up-front via ExchangeHelper.captureFailureOrigin() as soon as the exception is caught (RedeliveryErrorHandler, TryProcessor), before any failure processor runs. DefaultErrorRegistry now reads this snapshot for both handled and unhandled errors, falling back to the previous last-entry heuristic only when no error handler captured it (e.g. noErrorHandler()). Co-authored-by: Claude Sonnet 5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/catalog/docs/dead-letter-channel.adoc | 20 ++++++++++ .../org/apache/camel/catalog/models/doCatch.json | 4 +- .../org/apache/camel/catalog/models/doFinally.json | 4 +- .../apache/camel/catalog/models/errorHandler.json | 4 +- .../apache/camel/catalog/models/onException.json | 4 +- .../org/apache/camel/spring/xml/errorHandler.json | 4 +- .../org/apache/camel/ExchangeConstantProvider.java | 4 +- .../src/main/java/org/apache/camel/Exchange.java | 8 ++++ .../java/org/apache/camel/ExchangePropertyKey.java | 6 +++ .../camel/impl/engine/DefaultErrorRegistry.java | 29 ++++++++------ .../modules/eips/pages/dead-letter-channel.adoc | 20 ++++++++++ .../META-INF/org/apache/camel/model/doCatch.json | 4 +- .../META-INF/org/apache/camel/model/doFinally.json | 4 +- .../org/apache/camel/model/errorHandler.json | 4 +- .../org/apache/camel/model/onException.json | 4 +- .../org/apache/camel/processor/CatchProcessor.java | 5 --- .../apache/camel/processor/FinallyProcessor.java | 7 +--- .../org/apache/camel/processor/TryProcessor.java | 9 +++++ .../ShareUnitOfWorkAggregationStrategy.java | 8 ++++ .../errorhandler/RedeliveryErrorHandler.java | 16 +++----- .../org/apache/camel/impl/ErrorRegistryTest.java | 44 ++++++++++++++++++++++ .../org/apache/camel/support/ExchangeHelper.java | 29 ++++++++++++++ 22 files changed, 199 insertions(+), 42 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dead-letter-channel.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dead-letter-channel.adoc index 132bceab0b0e..37d1a28e2fbc 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dead-letter-channel.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dead-letter-channel.adoc @@ -633,6 +633,26 @@ The `Exchange.FAILURE_ROUTE_ID` have the constant value `CamelFailureRouteId`. This allows, for example, you to fetch this information in your dead letter queue and use that for error reporting. +=== Which node failed? + +Camel also stores the id of the node (EIP processor) where the message failed, along with its source +code location (requires source location to be enabled). This information can be obtained in Java via: + +._Java-only: accessing the failure node id and location from the Exchange_ +[source,java] +---- +String failedNodeId = exchange.getProperty(Exchange.FAILURE_NODE_ID, String.class); +String failedLocation = exchange.getProperty(Exchange.FAILURE_LOCATION, String.class); +---- + +The `Exchange.FAILURE_NODE_ID` have the constant value `CamelFailureNodeId`, and `Exchange.FAILURE_LOCATION` +have the constant value `CamelFailureLocation`. + +This information is captured up-front, as soon as the exception is caught, so it always points to the node +that actually failed - even when the error handler (`onException`, `doCatch`, or the dead letter channel +itself) performs further processing of its own before the exchange is moved to the dead letter queue. + + === Control if redelivery is allowed during stopping/shutdown The option `allowRedeliveryWhileStopping` (default is `true`) controls whether redelivery is allowed or not, diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/doCatch.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/doCatch.json index fe1bcc91fd59..b1a537a05ede 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/doCatch.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/doCatch.json @@ -23,6 +23,8 @@ "exchangeProperties": { "CamelExceptionCaught": { "index": 0, "kind": "exchangeProperty", "displayName": "Exception Caught", "label": "producer", "required": false, "javaType": "java.lang.Exception", "deprecated": false, "autowired": false, "secret": false, "description": "Stores the caught exception due to a processing error of the current Exchange" }, "CamelFailureEndpoint": { "index": 1, "kind": "exchangeProperty", "displayName": "Failure Endpoint", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Endpoint URI where the Exchange failed during processing" }, - "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" } + "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" }, + "CamelFailureNodeId": { "index": 3, "kind": "exchangeProperty", "displayName": "Failure Node Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Node ID where the Exchange failed during processing" }, + "CamelFailureLocation": { "index": 4, "kind": "exchangeProperty", "displayName": "Failure Location", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Source code location where the Exchange failed during processing" } } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/doFinally.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/doFinally.json index d8b5516eb41e..0ece8a5dcdc7 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/doFinally.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/doFinally.json @@ -21,6 +21,8 @@ "exchangeProperties": { "CamelExceptionCaught": { "index": 0, "kind": "exchangeProperty", "displayName": "Exception Caught", "label": "producer", "required": false, "javaType": "java.lang.Exception", "deprecated": false, "autowired": false, "secret": false, "description": "Stores the caught exception due to a processing error of the current Exchange" }, "CamelFailureEndpoint": { "index": 1, "kind": "exchangeProperty", "displayName": "Failure Endpoint", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Endpoint URI where the Exchange failed during processing" }, - "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" } + "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" }, + "CamelFailureNodeId": { "index": 3, "kind": "exchangeProperty", "displayName": "Failure Node Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Node ID where the Exchange failed during processing" }, + "CamelFailureLocation": { "index": 4, "kind": "exchangeProperty", "displayName": "Failure Location", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Source code location where the Exchange failed during processing" } } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/errorHandler.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/errorHandler.json index bea634af9bb9..12e4b0912735 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/errorHandler.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/errorHandler.json @@ -18,6 +18,8 @@ "exchangeProperties": { "CamelExceptionCaught": { "index": 0, "kind": "exchangeProperty", "displayName": "Exception Caught", "label": "producer", "required": false, "javaType": "java.lang.Exception", "deprecated": false, "autowired": false, "secret": false, "description": "Stores the caught exception due to a processing error of the current Exchange" }, "CamelFailureEndpoint": { "index": 1, "kind": "exchangeProperty", "displayName": "Failure Endpoint", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Endpoint URI where the Exchange failed during processing" }, - "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" } + "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" }, + "CamelFailureNodeId": { "index": 3, "kind": "exchangeProperty", "displayName": "Failure Node Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Node ID where the Exchange failed during processing" }, + "CamelFailureLocation": { "index": 4, "kind": "exchangeProperty", "displayName": "Failure Location", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Source code location where the Exchange failed during processing" } } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/onException.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/onException.json index 1063517cde30..355a81d775dc 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/onException.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/onException.json @@ -32,6 +32,8 @@ "exchangeProperties": { "CamelExceptionCaught": { "index": 0, "kind": "exchangeProperty", "displayName": "Exception Caught", "label": "producer", "required": false, "javaType": "java.lang.Exception", "deprecated": false, "autowired": false, "secret": false, "description": "Stores the caught exception due to a processing error of the current Exchange" }, "CamelFailureEndpoint": { "index": 1, "kind": "exchangeProperty", "displayName": "Failure Endpoint", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Endpoint URI where the Exchange failed during processing" }, - "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" } + "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" }, + "CamelFailureNodeId": { "index": 3, "kind": "exchangeProperty", "displayName": "Failure Node Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Node ID where the Exchange failed during processing" }, + "CamelFailureLocation": { "index": 4, "kind": "exchangeProperty", "displayName": "Failure Location", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Source code location where the Exchange failed during processing" } } } diff --git a/components/camel-spring-parent/camel-spring-xml/src/generated/resources/META-INF/org/apache/camel/spring/xml/errorHandler.json b/components/camel-spring-parent/camel-spring-xml/src/generated/resources/META-INF/org/apache/camel/spring/xml/errorHandler.json index 5de738fc2e52..090a8ea0ccc1 100644 --- a/components/camel-spring-parent/camel-spring-xml/src/generated/resources/META-INF/org/apache/camel/spring/xml/errorHandler.json +++ b/components/camel-spring-parent/camel-spring-xml/src/generated/resources/META-INF/org/apache/camel/spring/xml/errorHandler.json @@ -34,6 +34,8 @@ "exchangeProperties": { "CamelExceptionCaught": { "index": 0, "kind": "exchangeProperty", "displayName": "Exception Caught", "label": "producer", "required": false, "javaType": "java.lang.Exception", "deprecated": false, "autowired": false, "secret": false, "description": "Stores the caught exception due to a processing error of the current Exchange" }, "CamelFailureEndpoint": { "index": 1, "kind": "exchangeProperty", "displayName": "Failure Endpoint", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Endpoint URI where the Exchange failed during processing" }, - "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" } + "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" }, + "CamelFailureNodeId": { "index": 3, "kind": "exchangeProperty", "displayName": "Failure Node Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Node ID where the Exchange failed during processing" }, + "CamelFailureLocation": { "index": 4, "kind": "exchangeProperty", "displayName": "Failure Location", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Source code location where the Exchange failed during processing" } } } diff --git a/core/camel-api/src/generated/java/org/apache/camel/ExchangeConstantProvider.java b/core/camel-api/src/generated/java/org/apache/camel/ExchangeConstantProvider.java index 9b93e7bb3081..a9d27b5a2870 100644 --- a/core/camel-api/src/generated/java/org/apache/camel/ExchangeConstantProvider.java +++ b/core/camel-api/src/generated/java/org/apache/camel/ExchangeConstantProvider.java @@ -17,7 +17,7 @@ public class ExchangeConstantProvider { private static final Map<String, String> MAP; static { - Map<String, String> map = new HashMap<>(156); + Map<String, String> map = new HashMap<>(158); map.put("ACTIVITY_SPAN_TAGS", "CamelActivitySpanTags"); map.put("AGGREGATED_COLLECTION_GUARD", "CamelAggregatedCollectionGuard"); map.put("AGGREGATED_COMPLETED_BY", "CamelAggregatedCompletedBy"); @@ -61,6 +61,8 @@ public class ExchangeConstantProvider { map.put("EXCEPTION_HANDLED", "CamelExceptionHandled"); map.put("FAILURE_ENDPOINT", "CamelFailureEndpoint"); map.put("FAILURE_HANDLED", "CamelFailureHandled"); + map.put("FAILURE_LOCATION", "CamelFailureLocation"); + map.put("FAILURE_NODE_ID", "CamelFailureNodeId"); map.put("FAILURE_ROUTE_ID", "CamelFailureRouteId"); map.put("FATAL_FALLBACK_ERROR_HANDLER", "CamelFatalFallbackErrorHandler"); map.put("FILE_CONTENT_TYPE", "CamelFileContentType"); diff --git a/core/camel-api/src/main/java/org/apache/camel/Exchange.java b/core/camel-api/src/main/java/org/apache/camel/Exchange.java index 74457a08e0e8..b5ca93d302d1 100644 --- a/core/camel-api/src/main/java/org/apache/camel/Exchange.java +++ b/core/camel-api/src/main/java/org/apache/camel/Exchange.java @@ -142,6 +142,14 @@ public interface Exchange extends VariableAware { description = "Route ID where the Exchange failed during processing", javaType = "String") String FAILURE_ROUTE_ID = "CamelFailureRouteId"; + @Metadata(label = "doCatch,doFinally,errorHandler,onException", + description = "Node ID where the Exchange failed during processing", + javaType = "String") + String FAILURE_NODE_ID = "CamelFailureNodeId"; + @Metadata(label = "doCatch,doFinally,errorHandler,onException", + description = "Source code location where the Exchange failed during processing", + javaType = "String") + String FAILURE_LOCATION = "CamelFailureLocation"; String FATAL_FALLBACK_ERROR_HANDLER = "CamelFatalFallbackErrorHandler"; String FILE_CONTENT_TYPE = "CamelFileContentType"; String FILE_LOCAL_WORK_PATH = "CamelFileLocalWorkPath"; diff --git a/core/camel-api/src/main/java/org/apache/camel/ExchangePropertyKey.java b/core/camel-api/src/main/java/org/apache/camel/ExchangePropertyKey.java index c3d9ed220f05..8a90ce54e5d7 100644 --- a/core/camel-api/src/main/java/org/apache/camel/ExchangePropertyKey.java +++ b/core/camel-api/src/main/java/org/apache/camel/ExchangePropertyKey.java @@ -62,6 +62,8 @@ public enum ExchangePropertyKey { FAILURE_ENDPOINT(Exchange.FAILURE_ENDPOINT), FAILURE_HANDLED(Exchange.FAILURE_HANDLED), FAILURE_ROUTE_ID(Exchange.FAILURE_ROUTE_ID), + FAILURE_NODE_ID(Exchange.FAILURE_NODE_ID), + FAILURE_LOCATION(Exchange.FAILURE_LOCATION), FATAL_FALLBACK_ERROR_HANDLER(Exchange.FATAL_FALLBACK_ERROR_HANDLER), FILE_EXCHANGE_FILE(Exchange.FILE_EXCHANGE_FILE), GROUPED_EXCHANGE(Exchange.GROUPED_EXCHANGE), @@ -175,6 +177,10 @@ public enum ExchangePropertyKey { return FAILURE_ENDPOINT; case Exchange.FAILURE_ROUTE_ID: return FAILURE_ROUTE_ID; + case Exchange.FAILURE_NODE_ID: + return FAILURE_NODE_ID; + case Exchange.FAILURE_LOCATION: + return FAILURE_LOCATION; case Exchange.FATAL_FALLBACK_ERROR_HANDLER: return FATAL_FALLBACK_ERROR_HANDLER; case Exchange.FILE_EXCHANGE_FILE: diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java index 2f72fd4b661f..72ab35cefe6c 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java @@ -128,18 +128,23 @@ public class DefaultErrorRegistry extends EventNotifierSupport implements ErrorR } String endpointUri = exchange.getProperty(ExchangePropertyKey.FAILURE_ENDPOINT, String.class); - // capture node id and location from the last message history entry - // (the historyNodeId on the exchange extension is cleared after the node finishes processing, - // so by the time the error event fires it is always null) - String toNode = null; - String location = null; - List<MessageHistory> history - = exchange.getProperty(ExchangePropertyKey.MESSAGE_HISTORY, List.class); - if (history != null && !history.isEmpty()) { - MessageHistory last = history.get(history.size() - 1); - if (last.getNode() != null) { - toNode = last.getNode().getId(); - location = LoggerHelper.getLineNumberLoggerName(last.getNode()); + // capture node id and location where the exchange actually failed + // (captured up-front by the error handler / doCatch, before any failure processor such as + // onException or a dead letter channel ran its own processing steps - otherwise those steps + // would also be recorded in the message history, and the last entry would no longer point to + // the node that actually failed) + String toNode = exchange.getProperty(ExchangePropertyKey.FAILURE_NODE_ID, String.class); + String location = exchange.getProperty(ExchangePropertyKey.FAILURE_LOCATION, String.class); + if (toNode == null) { + // fallback for error handlers that do not capture the failure origin up-front (such as + // noErrorHandler): derive it from the last message history entry instead + List<MessageHistory> history = exchange.getProperty(ExchangePropertyKey.MESSAGE_HISTORY, List.class); + if (history != null && !history.isEmpty()) { + MessageHistory last = history.get(history.size() - 1); + if (last.getNode() != null) { + toNode = last.getNode().getId(); + location = LoggerHelper.getLineNumberLoggerName(last.getNode()); + } } } diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/dead-letter-channel.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/dead-letter-channel.adoc index 132bceab0b0e..37d1a28e2fbc 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/dead-letter-channel.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/dead-letter-channel.adoc @@ -633,6 +633,26 @@ The `Exchange.FAILURE_ROUTE_ID` have the constant value `CamelFailureRouteId`. This allows, for example, you to fetch this information in your dead letter queue and use that for error reporting. +=== Which node failed? + +Camel also stores the id of the node (EIP processor) where the message failed, along with its source +code location (requires source location to be enabled). This information can be obtained in Java via: + +._Java-only: accessing the failure node id and location from the Exchange_ +[source,java] +---- +String failedNodeId = exchange.getProperty(Exchange.FAILURE_NODE_ID, String.class); +String failedLocation = exchange.getProperty(Exchange.FAILURE_LOCATION, String.class); +---- + +The `Exchange.FAILURE_NODE_ID` have the constant value `CamelFailureNodeId`, and `Exchange.FAILURE_LOCATION` +have the constant value `CamelFailureLocation`. + +This information is captured up-front, as soon as the exception is caught, so it always points to the node +that actually failed - even when the error handler (`onException`, `doCatch`, or the dead letter channel +itself) performs further processing of its own before the exchange is moved to the dead letter queue. + + === Control if redelivery is allowed during stopping/shutdown The option `allowRedeliveryWhileStopping` (default is `true`) controls whether redelivery is allowed or not, diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/doCatch.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/doCatch.json index fe1bcc91fd59..b1a537a05ede 100644 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/doCatch.json +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/doCatch.json @@ -23,6 +23,8 @@ "exchangeProperties": { "CamelExceptionCaught": { "index": 0, "kind": "exchangeProperty", "displayName": "Exception Caught", "label": "producer", "required": false, "javaType": "java.lang.Exception", "deprecated": false, "autowired": false, "secret": false, "description": "Stores the caught exception due to a processing error of the current Exchange" }, "CamelFailureEndpoint": { "index": 1, "kind": "exchangeProperty", "displayName": "Failure Endpoint", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Endpoint URI where the Exchange failed during processing" }, - "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" } + "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" }, + "CamelFailureNodeId": { "index": 3, "kind": "exchangeProperty", "displayName": "Failure Node Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Node ID where the Exchange failed during processing" }, + "CamelFailureLocation": { "index": 4, "kind": "exchangeProperty", "displayName": "Failure Location", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Source code location where the Exchange failed during processing" } } } diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/doFinally.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/doFinally.json index d8b5516eb41e..0ece8a5dcdc7 100644 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/doFinally.json +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/doFinally.json @@ -21,6 +21,8 @@ "exchangeProperties": { "CamelExceptionCaught": { "index": 0, "kind": "exchangeProperty", "displayName": "Exception Caught", "label": "producer", "required": false, "javaType": "java.lang.Exception", "deprecated": false, "autowired": false, "secret": false, "description": "Stores the caught exception due to a processing error of the current Exchange" }, "CamelFailureEndpoint": { "index": 1, "kind": "exchangeProperty", "displayName": "Failure Endpoint", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Endpoint URI where the Exchange failed during processing" }, - "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" } + "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" }, + "CamelFailureNodeId": { "index": 3, "kind": "exchangeProperty", "displayName": "Failure Node Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Node ID where the Exchange failed during processing" }, + "CamelFailureLocation": { "index": 4, "kind": "exchangeProperty", "displayName": "Failure Location", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Source code location where the Exchange failed during processing" } } } diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/errorHandler.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/errorHandler.json index bea634af9bb9..12e4b0912735 100644 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/errorHandler.json +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/errorHandler.json @@ -18,6 +18,8 @@ "exchangeProperties": { "CamelExceptionCaught": { "index": 0, "kind": "exchangeProperty", "displayName": "Exception Caught", "label": "producer", "required": false, "javaType": "java.lang.Exception", "deprecated": false, "autowired": false, "secret": false, "description": "Stores the caught exception due to a processing error of the current Exchange" }, "CamelFailureEndpoint": { "index": 1, "kind": "exchangeProperty", "displayName": "Failure Endpoint", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Endpoint URI where the Exchange failed during processing" }, - "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" } + "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" }, + "CamelFailureNodeId": { "index": 3, "kind": "exchangeProperty", "displayName": "Failure Node Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Node ID where the Exchange failed during processing" }, + "CamelFailureLocation": { "index": 4, "kind": "exchangeProperty", "displayName": "Failure Location", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Source code location where the Exchange failed during processing" } } } diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/onException.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/onException.json index 1063517cde30..355a81d775dc 100644 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/onException.json +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/onException.json @@ -32,6 +32,8 @@ "exchangeProperties": { "CamelExceptionCaught": { "index": 0, "kind": "exchangeProperty", "displayName": "Exception Caught", "label": "producer", "required": false, "javaType": "java.lang.Exception", "deprecated": false, "autowired": false, "secret": false, "description": "Stores the caught exception due to a processing error of the current Exchange" }, "CamelFailureEndpoint": { "index": 1, "kind": "exchangeProperty", "displayName": "Failure Endpoint", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Endpoint URI where the Exchange failed during processing" }, - "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" } + "CamelFailureRouteId": { "index": 2, "kind": "exchangeProperty", "displayName": "Failure Route Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Route ID where the Exchange failed during processing" }, + "CamelFailureNodeId": { "index": 3, "kind": "exchangeProperty", "displayName": "Failure Node Id", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Node ID where the Exchange failed during processing" }, + "CamelFailureLocation": { "index": 4, "kind": "exchangeProperty", "displayName": "Failure Location", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "autowired": false, "secret": false, "description": "Source code location where the Exchange failed during processing" } } } diff --git a/core/camel-core-processor/src/main/java/org/apache/camel/processor/CatchProcessor.java b/core/camel-core-processor/src/main/java/org/apache/camel/processor/CatchProcessor.java index 8a3bed1e1b0f..30bf2a219b1a 100644 --- a/core/camel-core-processor/src/main/java/org/apache/camel/processor/CatchProcessor.java +++ b/core/camel-core-processor/src/main/java/org/apache/camel/processor/CatchProcessor.java @@ -154,11 +154,6 @@ public class CatchProcessor extends BaseDelegateProcessorSupport if (exchange.getProperty(ExchangePropertyKey.FAILURE_ENDPOINT) == null) { exchange.setProperty(ExchangePropertyKey.FAILURE_ENDPOINT, exchange.getProperty(ExchangePropertyKey.TO_ENDPOINT)); } - // and store the route id so we know in which route we failed - String routeId = ExchangeHelper.getAtRouteId(exchange); - if (routeId != null) { - exchange.setProperty(ExchangePropertyKey.FAILURE_ROUTE_ID, routeId); - } // give the rest of the pipeline another chance exchange.setProperty(ExchangePropertyKey.EXCEPTION_HANDLED, true); exchange.setProperty(ExchangePropertyKey.EXCEPTION_CAUGHT, e); diff --git a/core/camel-core-processor/src/main/java/org/apache/camel/processor/FinallyProcessor.java b/core/camel-core-processor/src/main/java/org/apache/camel/processor/FinallyProcessor.java index 26c71d900105..60ace585b1c4 100644 --- a/core/camel-core-processor/src/main/java/org/apache/camel/processor/FinallyProcessor.java +++ b/core/camel-core-processor/src/main/java/org/apache/camel/processor/FinallyProcessor.java @@ -58,11 +58,6 @@ public class FinallyProcessor extends BaseDelegateProcessorSupport exchange.setProperty(ExchangePropertyKey.FAILURE_ENDPOINT, exchange.getProperty(ExchangePropertyKey.TO_ENDPOINT)); } - // and store the route id so we know in which route we failed - String routeId = ExchangeHelper.getAtRouteId(exchange); - if (routeId != null) { - exchange.setProperty(ExchangePropertyKey.FAILURE_ROUTE_ID, routeId); - } } // continue processing @@ -132,6 +127,8 @@ public class FinallyProcessor extends BaseDelegateProcessorSupport if (exception == null) { exchange.removeProperty(ExchangePropertyKey.FAILURE_ENDPOINT); exchange.removeProperty(ExchangePropertyKey.FAILURE_ROUTE_ID); + exchange.removeProperty(ExchangePropertyKey.FAILURE_NODE_ID); + exchange.removeProperty(ExchangePropertyKey.FAILURE_LOCATION); } else { // set exception back on exchange exchange.setException(exception); diff --git a/core/camel-core-processor/src/main/java/org/apache/camel/processor/TryProcessor.java b/core/camel-core-processor/src/main/java/org/apache/camel/processor/TryProcessor.java index 26dfe7b90e4e..af2c5ad10026 100644 --- a/core/camel-core-processor/src/main/java/org/apache/camel/processor/TryProcessor.java +++ b/core/camel-core-processor/src/main/java/org/apache/camel/processor/TryProcessor.java @@ -92,6 +92,7 @@ public class TryProcessor extends BaseProcessorSupport final AsyncCallback callback; final Iterator<Processor> processors; final Object lastHandled; + boolean failureOriginCaptured; public TryState(Exchange exchange, AsyncCallback callback) { this.exchange = exchange; @@ -104,6 +105,14 @@ public class TryProcessor extends BaseProcessorSupport @Override public void run() { if (continueRouting(processors, exchange)) { + // capture where the exchange failed as soon as the exception appears, before handing off to a + // doCatch/doFinally clause which is itself a channeled node and would otherwise add its own + // entry to the message history, hiding the node that actually failed + if (!failureOriginCaptured && exchange.getException() != null) { + failureOriginCaptured = true; + ExchangeHelper.captureFailureOrigin(exchange); + } + exchange.setProperty(ExchangePropertyKey.TRY_ROUTE_BLOCK, true); ExchangeHelper.prepareOutToIn(exchange); diff --git a/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/ShareUnitOfWorkAggregationStrategy.java b/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/ShareUnitOfWorkAggregationStrategy.java index 48b64d7a8b78..42b38ac12aa1 100644 --- a/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/ShareUnitOfWorkAggregationStrategy.java +++ b/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/ShareUnitOfWorkAggregationStrategy.java @@ -126,6 +126,14 @@ public final class ShareUnitOfWorkAggregationStrategy extends ServiceSupport imp answer.setProperty(ExchangePropertyKey.FAILURE_ROUTE_ID, newExchange.getProperty(ExchangePropertyKey.FAILURE_ROUTE_ID)); } + if (newExchange.getProperty(ExchangePropertyKey.FAILURE_NODE_ID) != null) { + answer.setProperty(ExchangePropertyKey.FAILURE_NODE_ID, + newExchange.getProperty(ExchangePropertyKey.FAILURE_NODE_ID)); + } + if (newExchange.getProperty(ExchangePropertyKey.FAILURE_LOCATION) != null) { + answer.setProperty(ExchangePropertyKey.FAILURE_LOCATION, + newExchange.getProperty(ExchangePropertyKey.FAILURE_LOCATION)); + } if (newExchange.getExchangeExtension().getErrorHandlerHandled() != null) { answer.getExchangeExtension() .setErrorHandlerHandled(newExchange.getExchangeExtension().getErrorHandlerHandled()); diff --git a/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java b/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java index a4b1fba908b1..6e325518c49b 100644 --- a/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java +++ b/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java @@ -831,11 +831,9 @@ public abstract class RedeliveryErrorHandler extends ErrorHandlerSupport exchange.setProperty(ExchangePropertyKey.EXCEPTION_CAUGHT, e); } - // store where the exception happened - Route rc = ExchangeHelper.getRoute(exchange); - if (rc != null) { - exchange.setProperty(ExchangePropertyKey.FAILURE_ROUTE_ID, rc.getRouteId()); - } + // store the route, node and location where the exception happened, before any failure processor + // (onException, dead letter channel, ...) runs and adds its own entries to the message history + ExchangeHelper.captureFailureOrigin(exchange); } /** @@ -1381,11 +1379,9 @@ public abstract class RedeliveryErrorHandler extends ErrorHandlerSupport redeliveryCounter = incrementRedeliveryCounter(exchange); - // store where the exception happened - Route rc = ExchangeHelper.getRoute(exchange); - if (rc != null) { - exchange.setProperty(ExchangePropertyKey.FAILURE_ROUTE_ID, rc.getRouteId()); - } + // store the route, node and location where the exception happened, before any failure processor + // (onException, dead letter channel, ...) runs and adds its own entries to the message history + ExchangeHelper.captureFailureOrigin(exchange); } /** diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java index 847ae8702a41..c83ffe2c1741 100644 --- a/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java @@ -27,6 +27,7 @@ import org.apache.camel.spi.ErrorRegistry; import org.apache.camel.spi.ErrorRegistryView; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -169,6 +170,36 @@ public class ErrorRegistryTest extends ContextTestSupport { assertNotNull(entry.getToNode(), "Node id should be captured for unhandled errors"); } + @Test + public void testErrorRegistryHandledErrorReportsOriginNode() throws Exception { + getMockEndpoint("mock:handlerStep").expectedMessageCount(1); + + template.sendBody("direct:handledWithSteps", "Hello World"); + + assertMockEndpointsSatisfied(); + + BacklogErrorEventMessage entry = context.getErrorRegistry().browse().iterator().next(); + assertThat(entry.isHandled()).isTrue(); + assertThat(entry.getToNode()) + .as("toNode should point to the node that actually failed, not a node touched by the onException handler") + .isEqualTo("throwOrigin"); + } + + @Test + public void testErrorRegistryDoCatchReportsOriginNode() throws Exception { + getMockEndpoint("mock:catchStep").expectedMessageCount(1); + + template.sendBody("direct:doCatchWithSteps", "Hello World"); + + assertMockEndpointsSatisfied(); + + BacklogErrorEventMessage entry = context.getErrorRegistry().browse().iterator().next(); + assertThat(entry.isHandled()).isTrue(); + assertThat(entry.getToNode()) + .as("toNode should point to the node that actually failed, not a node touched by the doCatch block") + .isEqualTo("throwInTry"); + } + @Test public void testErrorRegistryCapturesEndpointUri() throws Exception { getMockEndpoint("mock:dead").expectedMessageCount(1); @@ -298,6 +329,19 @@ public class ErrorRegistryTest extends ContextTestSupport { .setProperty("myProp", constant("propValue")) .setHeader("myHeader", constant("headerValue")) .throwException(new IllegalArgumentException("Data error")); + + from("direct:handledWithSteps").routeId("handledWithSteps") + .onException(IllegalArgumentException.class).handled(true).to("mock:handlerStep").end() + .to("mock:before") + .throwException(new IllegalArgumentException("Handled with steps")).id("throwOrigin"); + + from("direct:doCatchWithSteps").routeId("doCatchWithSteps") + .doTry() + .to("mock:before") + .throwException(new IllegalArgumentException("Handled in doCatch")).id("throwInTry") + .doCatch(IllegalArgumentException.class) + .to("mock:catchStep") + .end(); } }; } diff --git a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java b/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java index 2eaab1737d4a..1f4e32d81067 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java @@ -1089,6 +1089,35 @@ public final class ExchangeHelper { return uow != null ? uow.getRoute() : null; } + /** + * Captures where the Exchange failed - the route id, node id, and source location - as the + * {@link Exchange#FAILURE_ROUTE_ID}, {@link Exchange#FAILURE_NODE_ID} and {@link Exchange#FAILURE_LOCATION} + * properties, based on the current route and the last entry in the message history. + * <p/> + * This must be called as soon as the exception is caught (handled or not), before any exception handler + * (onException, doCatch, dead letter channel, ...) has a chance to run any of its own processing steps - otherwise + * those steps would also be recorded in the message history, and the last entry would no longer point to the node + * that actually failed. + * + * @param exchange the exchange that failed + */ + @SuppressWarnings("unchecked") + public static void captureFailureOrigin(Exchange exchange) { + Route rc = getRoute(exchange); + if (rc != null) { + exchange.setProperty(ExchangePropertyKey.FAILURE_ROUTE_ID, rc.getRouteId()); + } + List<MessageHistory> history = exchange.getProperty(ExchangePropertyKey.MESSAGE_HISTORY, List.class); + if (history != null && !history.isEmpty()) { + MessageHistory last = history.get(history.size() - 1); + if (last.getNode() != null) { + exchange.setProperty(ExchangePropertyKey.FAILURE_NODE_ID, last.getNode().getId()); + exchange.setProperty(ExchangePropertyKey.FAILURE_LOCATION, + LoggerHelper.getLineNumberLoggerName(last.getNode())); + } + } + } + /** * Sets the body in message in the exchange taking the exchange pattern into consideration. If the pattern is out * capable, then the body is set outbound message. Otherwise it is set on the inbound message.
