This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 9edfc97639e5 CAMEL-25060: camel-base - Report a circular reference
through an optional property placeholder (#26943)
9edfc97639e5 is described below
commit 9edfc97639e5670623ee0cefecf21d68b9e9b4d8
Author: allthingssecurity <[email protected]>
AuthorDate: Mon Sep 28 14:13:05 2026 +0530
CAMEL-25060: camel-base - Report a circular reference through an optional
property placeholder (#26943)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../properties/DefaultPropertiesParser.java | 13 +-
.../PropertiesComponentCircularReferenceTest.java | 146 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 11 ++
3 files changed, 162 insertions(+), 8 deletions(-)
diff --git
a/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesParser.java
b/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesParser.java
index 43edd497bb15..16e9b394fa73 100644
---
a/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesParser.java
+++
b/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesParser.java
@@ -173,14 +173,11 @@ public class DefaultPropertiesParser implements
PropertiesParser {
Property property;
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 [" +
property.getKey() + "] from text: " + input);
- }
+ // Check for circular references, also for optional keys,
as a circular reference can never be
+ // resolved (returning the text unresolved would make the
caller parse the same placeholder again,
+ // and again, for ever)
+ throw new IllegalArgumentException(
+ "Circular reference detected with key [" +
property.getKey() + "] from text: " + input);
}
if (propertiesComponent != null) {
diff --git
a/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesComponentCircularReferenceTest.java
b/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesComponentCircularReferenceTest.java
new file mode 100644
index 000000000000..a2c919cc1836
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesComponentCircularReferenceTest.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.properties;
+
+import java.time.Duration;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class PropertiesComponentCircularReferenceTest extends
ContextTestSupport {
+
+ // a circular reference through an optional placeholder used to make the
parser loop for ever
+ private static final Duration TIMEOUT = Duration.ofSeconds(10);
+
+ @Override
+ public boolean isUseRouteBuilder() {
+ return false;
+ }
+
+ @Test
+ public void testRequiredCircularReference() {
+ context.getPropertiesComponent().addInitialProperty("loop1",
"{{loop2}}");
+ context.getPropertiesComponent().addInitialProperty("loop2",
"{{loop1}}");
+ context.start();
+
+ assertCircularReference("{{loop1}}", "loop1");
+ }
+
+ @Test
+ public void testOptionalSelfReference() {
+ context.getPropertiesComponent().addInitialProperty("self",
"{{?self}}");
+ context.start();
+
+ assertCircularReference("{{self}}", "?self");
+ assertCircularReference("{{?self}}", "?self");
+ }
+
+ @Test
+ public void testOptionalMutualReference() {
+ context.getPropertiesComponent().addInitialProperty("a", "{{?b}}");
+ context.getPropertiesComponent().addInitialProperty("b", "{{?a}}");
+ context.start();
+
+ assertCircularReference("x{{a}}y", "?b");
+ }
+
+ @Test
+ public void testOptionalSelfReferenceWithDefaultValue() {
+ context.getPropertiesComponent().addInitialProperty("timeout",
"{{?timeout:5000}}");
+ context.start();
+
+ assertCircularReference("{{timeout}}", "?timeout:5000");
+ }
+
+ @Test
+ public void testOptionalSelfReferenceInEndpointUri() throws Exception {
+ // use its own context, so a start that does not return cannot block
the stop in tearDown
+ CamelContext camel = new DefaultCamelContext();
+ camel.getPropertiesComponent().addInitialProperty("timeout",
"{{?timeout:5000}}");
+ camel.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("timer:t?period={{timeout}}&repeatCount=1").to("mock:result");
+ }
+ });
+
+ Exception e = assertTimeoutPreemptively(TIMEOUT, () ->
assertThrows(Exception.class, camel::start));
+ camel.stop();
+ assertCauseMessage(e, "Circular reference detected with key
[?timeout:5000]");
+ }
+
+ @Test
+ public void testOptionalWithoutCircularReference() {
+ context.getPropertiesComponent().addInitialProperty("a",
"{{b}}-{{?c}}");
+ context.getPropertiesComponent().addInitialProperty("b", "B");
+ context.getPropertiesComponent().addInitialProperty("timeout",
"{{?override.timeout:5000}}");
+ context.getPropertiesComponent().addInitialProperty("delay",
"{{?override.delay}}");
+ context.start();
+
+ assertTimeoutPreemptively(TIMEOUT, () -> {
+ assertEquals("B-", context.resolvePropertyPlaceholders("{{a}}"));
+ // the same key twice is not a circular reference
+ assertEquals("B-B-",
context.resolvePropertyPlaceholders("{{a}}{{a}}"));
+ assertEquals("xy",
context.resolvePropertyPlaceholders("x{{?nope}}y"));
+ assertNull(context.resolvePropertyPlaceholders("{{?nope}}"));
+ assertEquals("5000",
context.resolvePropertyPlaceholders("{{?nope:5000}}"));
+ assertEquals("5000",
context.resolvePropertyPlaceholders("{{timeout}}"));
+ assertEquals("x-y",
context.resolvePropertyPlaceholders("x-{{delay}}y"));
+ });
+ }
+
+ @Test
+ public void testOptionalWithoutCircularReferenceInEndpointUri() throws
Exception {
+ context.getPropertiesComponent().addInitialProperty("timeout",
"{{?override.timeout:5000}}");
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:start").to("mock:result?retainFirst={{?maxKeep}}&resultWaitTime={{timeout}}");
+ }
+ });
+ context.start();
+
+ assertNotNull(context.hasEndpoint("mock:result?resultWaitTime=5000"),
+ "Endpoints: " + context.getEndpointRegistry().keySet());
+ }
+
+ private void assertCircularReference(String text, String key) {
+ IllegalArgumentException e = assertTimeoutPreemptively(TIMEOUT,
+ () -> assertThrows(IllegalArgumentException.class, () ->
context.resolvePropertyPlaceholders(text)));
+ assertTrue(e.getMessage().startsWith("Circular reference detected with
key [" + key + "]"), e.getMessage());
+ }
+
+ private static void assertCauseMessage(Throwable e, String message) {
+ for (Throwable t = e; t != null; t = t.getCause()) {
+ if (t.getMessage() != null && t.getMessage().contains(message)) {
+ return;
+ }
+ }
+ fail("Expected a cause with message containing: " + message + " but
was: " + e);
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index b53995cf6da1..3361cfdc7f05 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -749,6 +749,17 @@ placeholder belongs there:
entirely at runtime and may legitimately carry a placeholder that comes from
configuration, so they
continue to resolve `{{...}}` in the computed recipient.
+=== camel-core - a circular reference through an optional property placeholder
is an error
+
+A property placeholder that refers back to itself through an optional
placeholder, such as
+`timeout={{?timeout:5000}}`, or two optional placeholders that refer to each
other (`a={{?b}}` and `b={{?a}}`),
+now fail with the same `Circular reference detected` error as a circular
reference through required placeholders.
+Before, resolving such a placeholder never returned, and the thread (for
example the one starting the `CamelContext`)
+kept running at full CPU without any log. No configuration that worked before
is affected.
+
+To take a value from somewhere else and otherwise a default, use another key
for the override, such as
+`timeout={{?timeout.override:5000}}`.
+
=== camel-core - XmlConverter SAX parser factory
`XmlConverter.createSAXParserFactory()` now also disables external parameter
entities and external