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

Reply via email to