shashank created CAMEL-25060:
--------------------------------
Summary: Property placeholders: an optional circular reference
such as timeout={{?timeout:5000}} makes the parser loop for ever (CamelContext
start hangs at 100 % CPU)
Key: CAMEL-25060
URL: https://issues.apache.org/jira/browse/CAMEL-25060
Project: Camel
Issue Type: Bug
Components: camel-core
Reporter: shashank
{{DefaultPropertiesParser.ParsingContext.doParseNested}}
({{core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesParser.java}})
detects a circular reference by the keys that are already being resolved on
the current branch:
{code:java}
while ((property = readProperty(prevKey, answer)) != null) {
if (replacedPropertyKeys.contains(property.getKey())) {
// Check for circular references (skip optional)
boolean optional = property.getKey().startsWith(OPTIONAL_TOKEN);
if (optional) {
break;
} else {
throw new IllegalArgumentException("Circular reference detected
with key [" + ...);
}
}
...
String parsed = doParseNested(property.getKey(), property.getValue(),
newReplaced);
answer = before + parsed + after;
}
{code}
For an optional key the {{break}} returns the text with the placeholder still
in it. The caller puts that text back into its own {{answer}}, and its loop
reads the same placeholder again. Its set of keys is one key smaller, so it
does not see a cycle, resolves the placeholder again, gets the same text back,
and so on for ever. A required cycle is reported with the exception; an
optional one hangs the thread at 100 % CPU without any log.
A plausible way to get there is a property that should take an override from
somewhere else and otherwise a default, written with its own name:
timeout=\{\{?timeout:5000\}\}. Two optional properties that refer to each other
(a=\{\{?b\}\} and b=\{\{?a\}\}) hang as well.
h3. Reproduction
A standalone program against main 65f315628; each case runs in its own thread
with a 3 second limit:
{noformat}
self={{?self}}; resolvePropertyPlaceholders("{{self}}") -> still
running after 3005 ms
a={{?b}}, b={{?a}}; resolvePropertyPlaceholders("x{{a}}y") -> still
running after 3004 ms
timeout={{?timeout:5000}}; resolvePropertyPlaceholders("{{timeout}}") -> still
running after 3002 ms
same, route from("timer:t?period={{timeout}}&repeatCount=1"),
CamelContext.start() -> still running after 3004 ms
stack: DefaultPropertiesParser doGetPropertyValue:468 getPropertyValue:391
readProperty:243 doParseNested:174
doParseNested:202 doParseNested:202 parse:121 parseUri:73
controls:
loop1={{loop2}}, loop2={{loop1}} -> IllegalArgumentException: Circular
reference detected with key [loop1]
x{{?nope}}y -> xy
a={{b}}-{{?c}}, b=B: {{a}} -> B-
{noformat}
A thread dump of the first case after 160 s shows the main thread RUNNABLE with
159 s of CPU time in the same frames. A jqwik property over cycles of 1 to 3
keys fails on the first cycle whose closing reference is optional, and a Lean
model of {{readProperty}} and {{doParseNested}} proves that resolving the self
reference runs out of fuel for every amount of fuel: one iteration of the loop
at the second level returns it to exactly the same state.
h3. Why the break can never return a result
The set holds the keys of the placeholders being resolved above the current one
on the same branch (it is copied for each branch), so the check only fires for
a real cycle. When a level breaks, it returns its text to the level above,
whose next {{readProperty}} finds the same placeholder first. If that level's
own set does not hold the key, it is the level that resolves that key, and it
recurses into exactly the same state as before. If its set holds the key too,
it breaks as well, and the same happens one level up. The top level starts with
an empty set, so some level always loops. Unless a properties function gives a
different value on each call, no configuration can get a result through this
{{break}}, so any change that makes it end cannot break a configuration that
works today.
Optional placeholders came with CAMEL-16302 (3.9.0), which added this
{{break}}; the comment says "skip optional", so the intent was to skip the
cycle, not to hang. The {{break}} is in camel-3.9.0 and every later release
checked (3.14.0, 4.0.0, 4.14.0, 4.18.0) and on main. The documentation of
optional placeholders ("Using optional property placeholders" in
{{using-propertyplaceholder.adoc}}) says what happens to a missing optional key
and says nothing about cycles.
h3. Proposed fix
Report the circular reference for optional keys too, with the same
{{IllegalArgumentException}} ("Circular reference detected with key
\[?timeout:5000\] ...") as for required keys, by removing the {{optional}}
special case. The configuration is wrong either way, and the user gets a clear
message at startup instead of a hung thread.
The alternative, treating the cyclic placeholder like a missing optional key,
would have to use the default after the first {{:}} when there is one, and
otherwise return what a missing optional key gives (dropped from the text, or
the option removed from an endpoint URI). That is more code, needs its own
tests for both modes, and hides a configuration error. To take an override and
otherwise a default, another key works today:
timeout=\{\{?timeout.override:5000\}\}.
Tests: a new test with {{assertTimeoutPreemptively}}: the self reference, the
mutual reference and the self reference with a default each end with the
circular reference error, also as a timer endpoint option on
{{CamelContext.start()}}; a required cycle still gives the error; non-cyclic
optional placeholders, defaults and a missing optional key in an endpoint URI
keep working.
Duplicate check (2026-09-27): JIRA {{text ~ "DefaultPropertiesParser"}}
(CAMEL-24776, CAMEL-23652, CAMEL-21484 and older), {{text ~ "Circular reference
detected"}} (CAMEL-11273, CAMEL-11668, CAMEL-11497), {{summary ~ placeholder
AND (hang OR loop OR circular OR infinite)}}, and optional placeholder issues
since 2024: nothing related. GitHub PR searches {{DefaultPropertiesParser}},
{{circular reference placeholder}} and {{optional placeholder}}: nothing
related. Not reported.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)