Avoid Expired Token Retry Loop in Apache Camel
Apache Camel offers powerful error-handling capabilities. By defining an onException policy, you can trigger automatic retries for any failed activity within your route.
from(timer("get-articles").repeatCount(1))
.onException(Exception.class)
.maximumRedeliveries(-1)
.end()
.setHeader("Authorization", this::tokenManager)
.to(http("localhost:8080/articles"))
.to(stream("articles"));
In this example, the route retries the HTTP call indefinitely due to the -1 setting. This is useful for background processes where no client is waiting for a real-time response, provided you are certain the error is transient and the server will eventually recover.
The Pitfall: The Token Expiry Trap
There is a catch. If the failure persists longer than the lifetime of your authentication token, you will run into trouble.
Initially, the server might return a 500 Internal Server Error. However, once your token expires during the retry loop, the server will start returning 401 Unauthorized exceptions. Since the Authorization header was set before the retry loop started, Camel will keep retrying with the same expired token forever.
Here are three ways to solve this.
1. Using a Sub-Route
You can move the token generation and the API call into a sub-route. By doing this, the parent route's retry mechanism triggers the entire sub-route again, forcing a fresh token request.
Note: You must disable the error handler on the sub-route using
.errorHandler(noErrorHandler())so the exception propagates back to the parent route.
from(direct("get-articles"))
.onException(HttpOperationFailedException.class)
.maximumRedeliveries(-1)
.end()
.to(direct("call-api"))
.to(log("articles"));
from(direct("call-api"))
.errorHandler(noErrorHandler())
.setHeader("Authorization", method(tokenManager, "getToken"))
.to(https("example.org/articles"));
2. Leveraging onRedelivery
A cleaner approach is to use the onRedelivery processor. This allows you to execute logic—like updating a header—immediately before each retry attempt.
from(direct("get-articles"))
.onException(HttpOperationFailedException.class)
.maximumRedeliveries(-1)
.onRedelivery(e -> e.getIn().setHeader("Authorization", tokenManager.getToken()))
.end()
.setHeader("Authorization", method(tokenManager, "getToken"))
.to(https("example.org/articles"))
.to(log("articles"));
3. Conditional onException Blocks
If you want granular control, you can separate your error handling based on the HTTP status code using .onWhen(). This allows you to handle 401s differently than other server errors.
from(timer("get-articles").repeatCount(1))
// Handle 401 Unauthorized specifically
.onException(HttpOperationFailedException.class)
.onWhen(exchange -> exchange.getException(HttpOperationFailedException.class).getStatusCode() == 401)
.maximumRedeliveries(-1)
.onRedelivery(e -> e.getIn().setHeader("Authorization", getToken()))
.end()
// Handle all other HTTP failures
.onException(HttpOperationFailedException.class)
.onWhen(exchange -> exchange.getException(HttpOperationFailedException.class).getStatusCode() != 401)
.maximumRedeliveries(-1)
.end()
.setHeader("Authorization", this::getToken)
.to(http("localhost:8080/articles"))
.to(stream("out"));
By accounting for token lifetimes in your retry strategy, you ensure your integration routes remain resilient even during extended periods of downstream downtime.
The code is available on GitHub.