Error handling in Apache Camel

Scenarios

When coding an integration with Apache Camel, we need to be able to deal with many different kinds of error:
  • Bug / error in our own code, before we have communicated with the remote service.
  • Getting a connection to the remote service, but it returning an error response / code.
  • Failing to connect to the remote service.
  • Error inside our error handling code! e.g. you are inside a try catch block
  • Message is retried from DLQ, but a later message has already been sent.
  • Power goes down after message has been picked up.

Coding options available

We have multiple options for handling errors:
  1. Try catch block – for catching errors where we can do something useful. e.g. update a status to failed
  2. On exception – can be used with a retry policy. This will save the current message state and what step failed. It will then block the consumer thread and retry from the failed step when the configured redelivery time is up. This should not be used with long redelivery time periods as the thread is blocked. See https://camel.apache.org/manual/exception-clause.html
  3. Error handler – similar to on exception, can be used with a retry policy that will save message state and failing step, and retry. However with onException you can specify different policies for different exception types. See https://camel.apache.org/manual/error-handler.html
  4. JMS retries. These are configured ActiveMQ rather than Camel. In this case, the message is retried from the start of the route. This is useful if you want to retry after a long redelivery period, like 10 minutes, as the AMQ consumer thread is not blocked. Also, unlike on exception and an error handler, each time the broker retries the message, it increments a header. This means we can write logic to detect when a message has been retried a certain number of times, and then invoke error handling. (See below.)
  5. Filter that if retries have exceeded a threshold and invokes error handling logic. If the message has been retried multiple times, this suggests that it will never succeed. e.g. input data is invalid. We don’t want it to constantly refail back to the DLQ so we can detect the number of retries and then invoke whatever error handling logic is appropriate.
  6. JDBC save points. Within a single transaction you can record save points, and perform a partial rollback to one of these, if an error occurs. The approach you take to errors depends on whether you think the integration module needs to do anything when an error occurs. If there is nothing useful that the module can do, you can permit the message to go straight to the DLQ. If you need to implement some error handling in the module, you can wrap the integration code in a block, with one or more blocks for the error conditions.

On exception

Can be used with a short redelivery period to make the route retry. This is useful in the case of a temporary network problem. You place the config inside your camel context, but outside of the routes.
NOTE: This does NOT retry from the start of the route! Camel saves a copy of the message with all headers and properties, and retries the step that failed!
<onException>
    <exception>java.io.IOException</exception>
    <redeliveryPolicy redeliveryDelay="10000" maximumRedeliveries="5"/>
    <handled>
        <constant>false</constant>
    </handled>
    <log loggingLevel="ERROR" message="Failure processing, giving up with exception: ${exception}"/>
</onException>

Using doCatch and doWhen

Camel actually has a more powerful catch mechanism than plain Java, as you can specify not just an exception type, but additional conditions. e.g.
<doTry>
    <to uri="cxfrs:bean:someCxfClient" pattern="InOut"/>
    <doCatch>
        <exception>org.apache.camel.component.cxf.CxfOperationException</exception>
        <onWhen>
            <simple>${exception.statusCode} == 401</simple>
        </onWhen>
Note you MUST use onWhen here. If you use something like filter or choice you have a code bug – you will catch all exceptions of the specified type, but only handle some of them!

Catching an error, making changes and retrying

This is a common pattern. Consider a route which does the following:
  1. Performs a login.
  2. Stores login credentials in cache.
  3. Connects to remote service.
We always need to consider the possibility that the cached credentials may be invalid. In this case, we need to:
  1. Identify the specific error or return code that says the credentials are invalid.
  2. Clear the cache.
  3. Login again.
  4. Retry the remote call.
You might think that you could catch the error, clear the cache, and then rethrow the exception so that your error handler will retry the message. This won’t work! If you have added the new login details to the message, they will be ignored, because the error handler will be retrying with the saved copy of the message that was the input to the failing step! There are two ways to deal with this problem:
  1. Manual approach. Simply catch the error, make the appropriate changes, then call the remote service again. You may need to set up headers for the remote service, so you might find this easier if you move the code for setting up the headers and making the call to its own route.
  2. Group together the steps you need to be rerun into their own route, and mark this as having no error handler. Then the exception gets propagated back up to the calling route. When this gets handled by an error handler, it will retry from the start of the second route. In the caching example, you would group together getting the auth token from the cache and then making the remote call.

Filter to detect number of retries

When you retry from the AMQ broker, it updates a message header with the retry number. You can use this to detect when a certain number of retries have been attempted, and invoke your error handling code. You should place this filter as close as possible to the top of your route. If not, the code could fail again before hitting the filter, and end up in the DLQ. Sample:
<log id="logRetryCount" loggingLevel="INFO" message="JMS delivery count: ${header.JMSXDeliveryCount}"/>
<filter>
    <simple>$simple{header.JMSXDeliveryCount} > {{jms.max.retries}}</simple>
    <log message="Retry limit has been reached" loggingLevel="INFO"/>
<!-- do your error handling in here, like setting the status to failed, or placing the message on a dedicated failure queue -->
    <stop/>
</filter>
I found in testing that when I tried to set the JMS header, it seemed to be ignored. However you can set it in your test by adding an advice by weaving in extra code. As long as you have a step in the route with an id, you can insert extra code before or after it. In the sample above, we have a log statement just before the filter, so in the test code we have:
AdviceWith.adviceWith(camelContext, "myCamelRoute", a -> {
    a.weaveById("logRetryCount").before()
            .process(exchange -> {
                if (maxRetriesExceeded) {
                    exchange.getIn().setHeader("JMSXDeliveryCount", 6);
                }
            });
});
Setting the header to the retry count only makes sense if the route handles the error. But if you need the message in the DLQ, you would re-throw it. To test this behaviour, just configure a parameter for checking the delivery count and set it to 2 for tests which retry exactly once.

Exceptions and error codes

When throwing exceptions, generally each different exception should have a different message, and potentially a unique error code. This makes it far easier to debug real failures, as you can easily find the section of code which threw the exception.

See Also

Other posts on Camel: Type conversion in Camel
This entry was posted in Camel, Java and tagged , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

HTML tags are not allowed.

517,760 Spambots Blocked by Simple Comments