Skip to content

CAMEL-23469: Fix infinite redelivery loop when child route removes headers#23090

Merged
Croway merged 4 commits into
apache:mainfrom
Croway:fix/redelivery-counter-reset
May 10, 2026
Merged

CAMEL-23469: Fix infinite redelivery loop when child route removes headers#23090
Croway merged 4 commits into
apache:mainfrom
Croway:fix/redelivery-counter-reset

Conversation

@Croway
Copy link
Copy Markdown
Contributor

@Croway Croway commented May 8, 2026

Summary

  • Fix RedeliveryErrorHandler.incrementRedeliveryCounter() to use the internal redeliveryCounter field as the authoritative counter instead of reading from the CamelRedeliveryCounter exchange header, which user routes can remove (e.g., via removeHeaders("*"))
  • Fix prepareExchangeForRedelivery() to restore redelivery headers from internal state rather than saving/restoring from exchange headers — resolves a longstanding TODO present since Camel 2.8
  • Add regression test (RedeliverToSubRouteRemoveHeadersTest) with two test methods: one verifying the infinite loop is fixed, one verifying onRedelivery processors see the correct counter

Scenario: parent route with onException/maximumRedeliveries(3) calls a child route (via direct:) that uses NoErrorHandler and removeHeaders("*"). The removeHeaders wipes CamelRedeliveryCounter during each attempt. Before this fix, the counter reset to 1 every time, causing an infinite redelivery loop.

The defensive-copy mechanism (copyFrom(original)) was introduced in Camel 2.8.0, but incrementRedeliveryCounter() was never updated to use the internal field — it kept reading from the header as it had since before the defensive copy existed. In Camel 2.7, prepareExchangeForRedelivery() did not copy from the original, so headers survived intact and this bug did not manifest.


Fix three instances of internal state being read from mutable exchange message headers instead of authoritative internal fields, making them vulnerable to removeHeaders("*") in sub-routes.

  • decrementRedeliveryCounter(): Moved from the outer RedeliveryErrorHandler class into RedeliveryTask where the internal redeliveryCounter field is accessible. Uses the same Math.max(header, field) pattern as incrementRedeliveryCounter() to respect nested error handler counters while falling back to the internal field when headers are wiped.
  • PollEnricher save/restore: Added redeliveryCounter and redeliveryMaxCounter fields to ExchangeExtension so redelivery state survives removeHeaders("*"). RedeliveryErrorHandler populates these fields alongside headers. PollEnricher now reads from ExchangeExtension instead of headers for its save/restore around aggregation with bridgeErrorHandler.
  • SagaProcessor coordinator ID: The Long-Running-Action header is the sole storage for the saga coordinator ID. If removed, saga coordination fails silently (REQUIRED), throws (MANDATORY), or breaks compensation (REQUIRES_NEW). Now dual-stored in ExchangeExtension alongside the header. getCurrentSagaCoordinator() reads from extension first, falls back to the header for LRA protocol interoperability.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@github-actions github-actions Bot added the core label May 8, 2026
@Croway Croway marked this pull request as draft May 8, 2026 15:21
@Croway Croway force-pushed the fix/redelivery-counter-reset branch from cdc980d to 4e94987 Compare May 8, 2026 15:31
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

🧪 CI tested the following changed modules:

  • core/camel-api
  • core/camel-core-processor
  • core/camel-core
  • core/camel-support

ℹ️ Dependent modules were not tested because the total number of affected modules exceeded the threshold (50). Use the test-dependents label to force testing all dependents.

⚠️ Some tests are disabled on GitHub Actions (@DisabledIfSystemProperty(named = "ci.env.name")) and require manual verification:

  • core/camel-core: 2 test(s) disabled on GitHub Actions
Build reactor — dependencies compiled but only changed modules were tested (4 modules)
  • Camel :: API
  • Camel :: Core
  • Camel :: Core Processor
  • Camel :: Support

⚙️ View full build and test results

@davsclaus
Copy link
Copy Markdown
Contributor

yeah so Camel has historically stored such metadata as headers and/or exchange properties. EIPs usually use properties but the error handler was headers and as such it kinda got stuck there - end user can check those headers to track how many time attempt of redelivery and so on.

Now that the exchange has this rather new state stuff we can put it there, and copy over the info to the headers so they are read-only.

Comment thread core/camel-api/src/main/java/org/apache/camel/ExchangeExtension.java Outdated
@Croway Croway marked this pull request as ready for review May 10, 2026 08:44
Croway added 4 commits May 10, 2026 11:18
…aders

RedeliveryErrorHandler.incrementRedeliveryCounter() read the counter from
the CamelRedeliveryCounter exchange header. When a child route with
NoErrorHandler used removeHeaders("*"), the header was wiped during each
redelivery attempt, causing the counter to reset to 1 and loop forever.

Fix incrementRedeliveryCounter() and prepareExchangeForRedelivery() to
use the internal redeliveryCounter field as the authoritative source
instead of reading from the mutable exchange header.
Move decrementRedeliveryCounter() from the outer RedeliveryErrorHandler
class into RedeliveryTask where the internal redeliveryCounter field is
accessible. Uses the same Math.max(header, field) pattern as
incrementRedeliveryCounter() to respect nested error handler counters
while falling back to the internal field when headers are wiped.

Add redeliveryCounter and redeliveryMaxCounter fields to
ExchangeExtension so redelivery state survives removeHeaders("*").
RedeliveryErrorHandler now populates these fields alongside headers.
PollEnricher reads from ExchangeExtension instead of headers for its
save/restore pattern around aggregation with bridgeErrorHandler.
SagaProcessor stores the coordinator ID exclusively as the
Long-Running-Action message header. If a sub-route calls
removeHeaders("*"), the coordinator lookup returns null — causing
silent saga corruption (REQUIRED), hard failure (MANDATORY), or
broken compensation (REQUIRES_NEW).

Dual-store the coordinator ID in ExchangeExtension alongside the
header. getCurrentSagaCoordinator() reads from extension first, then
falls back to the header for LRA protocol interoperability.
InMemorySagaCoordinator also sets the extension field when creating
exchanges for compensation/completion.
These are now the official API for redelivery counter and saga
coordinator state. The corresponding headers are set for backward
compatibility.
@Croway Croway changed the title Fix infinite redelivery loop when child route removes headers CAMEL-23469: Fix infinite redelivery loop when child route removes headers May 10, 2026
@Croway Croway force-pushed the fix/redelivery-counter-reset branch from 3ba228b to 2bc34f9 Compare May 10, 2026 09:19
@Croway Croway merged commit 811de05 into apache:main May 10, 2026
6 checks passed
Croway added a commit that referenced this pull request May 10, 2026
…aders (#23090)

* CAMEL-23469: Fix infinite redelivery loop when child route removes headers

RedeliveryErrorHandler.incrementRedeliveryCounter() read the counter from
the CamelRedeliveryCounter exchange header. When a child route with
NoErrorHandler used removeHeaders("*"), the header was wiped during each
redelivery attempt, causing the counter to reset to 1 and loop forever.

Fix incrementRedeliveryCounter() and prepareExchangeForRedelivery() to
use the internal redeliveryCounter field as the authoritative source
instead of reading from the mutable exchange header.

* Fix redelivery state resilience to header removal

Move decrementRedeliveryCounter() from the outer RedeliveryErrorHandler
class into RedeliveryTask where the internal redeliveryCounter field is
accessible. Uses the same Math.max(header, field) pattern as
incrementRedeliveryCounter() to respect nested error handler counters
while falling back to the internal field when headers are wiped.

Add redeliveryCounter and redeliveryMaxCounter fields to
ExchangeExtension so redelivery state survives removeHeaders("*").
RedeliveryErrorHandler now populates these fields alongside headers.
PollEnricher reads from ExchangeExtension instead of headers for its
save/restore pattern around aggregation with bridgeErrorHandler.

* Fix saga coordinator ID resilience to header removal

SagaProcessor stores the coordinator ID exclusively as the
Long-Running-Action message header. If a sub-route calls
removeHeaders("*"), the coordinator lookup returns null — causing
silent saga corruption (REQUIRED), hard failure (MANDATORY), or
broken compensation (REQUIRES_NEW).

Dual-store the coordinator ID in ExchangeExtension alongside the
header. getCurrentSagaCoordinator() reads from extension first, then
falls back to the header for LRA protocol interoperability.
InMemorySagaCoordinator also sets the extension field when creating
exchanges for compensation/completion.

* Address review: drop "internal" from ExchangeExtension javadocs

These are now the official API for redelivery counter and saga
coordinator state. The corresponding headers are set for backward
compatibility.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants