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 a3af37bd8b6c CAMEL-25387: camel-kamelet - A Kamelet whose template 
starts from the Kamelet itself fails with a clear message instead of a 
StackOverflowError (#27451)
a3af37bd8b6c is described below

commit a3af37bd8b6c3c6c3e820a17ceb23078d717d606
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 6 20:02:51 2026 +0200

    CAMEL-25387: camel-kamelet - A Kamelet whose template starts from the 
Kamelet itself fails with a clear message instead of a StackOverflowError 
(#27451)
    
    Creating a Kamelet's route creates the Kamelets its template uses, on the 
same
    thread. A template that starts from kamelet:<itself> created the Kamelet 
again
    while creating it, until the stack overflowed: at startup the JVM died with 
only
    "Exception in thread main java.lang.StackOverflowError", on a dev-mode 
reload the
    warning said "due to: null". The component now tracks the Kamelets it is
    creating on the thread and fails when one is entered again, saying the 
template
    starts from kamelet:source. The reload warning names an error without a 
message
    by its class.
    
    
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../camel/component/kamelet/KameletComponent.java  | 39 ++++++++-
 .../camel/component/kamelet/KameletCycleTest.java  | 94 ++++++++++++++++++++++
 .../support/FileWatcherResourceReloadStrategy.java |  3 +-
 3 files changed, 133 insertions(+), 3 deletions(-)

diff --git 
a/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletComponent.java
 
b/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletComponent.java
index 36d1efa71e38..c3eccedc5a91 100644
--- 
a/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletComponent.java
+++ 
b/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletComponent.java
@@ -16,8 +16,10 @@
  */
 package org.apache.camel.component.kamelet;
 
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Deque;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -62,6 +64,9 @@ import static 
org.apache.camel.component.kamelet.Kamelet.PARAM_UUID;
 @Component(Kamelet.SCHEME)
 public class KameletComponent extends DefaultComponent {
 
+    /** The Kamelets whose route this thread is creating, innermost first: to 
find a Kamelet that uses itself. */
+    private static final ThreadLocal<Deque<String>> CREATING = 
ThreadLocal.withInitial(ArrayDeque::new);
+
     private static final Logger LOG = 
LoggerFactory.getLogger(KameletComponent.class);
 
     private final ReentrantLock lock = new ReentrantLock();
@@ -474,11 +479,41 @@ public class KameletComponent extends DefaultComponent {
 
         protected void doCreateRouteForEndpoint(KameletEndpoint endpoint, 
String parentRouteId, String parentProcessorId)
                 throws Exception {
+            final String templateId = endpoint.getTemplateId();
+            final String loc = endpoint.getLocation() != null ? 
endpoint.getLocation() : getLocation();
+            // creating a Kamelet's route creates the Kamelets its template 
uses, on this thread: one that is already
+            // being created is a cycle, which created routes until the stack 
overflowed (CAMEL-25387)
+            Deque<String> creating = CREATING.get();
+            if (creating.contains(templateId)) {
+                List<String> cycle = new ArrayList<>(creating);
+                Collections.reverse(cycle);
+                cycle = cycle.subList(cycle.indexOf(templateId), cycle.size());
+                cycle.add(templateId);
+                String msg = cycle.size() == 2
+                        ? "Kamelet " + templateId + " uses itself: its 
template uses kamelet:" + templateId
+                          + ", which creates the Kamelet again while creating 
it. The template of an action or a sink"
+                          + " starts from kamelet:source, the message the 
route sends to the Kamelet"
+                        : "Kamelets use each other in a cycle: " + 
String.join(" -> ", cycle)
+                          + ". Creating one creates the next, endlessly";
+                throw new FailedToCreateKameletException(templateId, loc, new 
IllegalArgumentException(msg));
+            }
+            creating.push(templateId);
+            try {
+                doCreateRouteForEndpoint(endpoint, parentRouteId, 
parentProcessorId, templateId, loc);
+            } finally {
+                creating.pop();
+                if (creating.isEmpty()) {
+                    CREATING.remove();
+                }
+            }
+        }
+
+        private void doCreateRouteForEndpoint(
+                KameletEndpoint endpoint, String parentRouteId, String 
parentProcessorId, String templateId, String loc)
+                throws Exception {
 
             final ModelCamelContext context = (ModelCamelContext) 
getCamelContext();
-            final String templateId = endpoint.getTemplateId();
             final String routeId = endpoint.getRouteId();
-            final String loc = endpoint.getLocation() != null ? 
endpoint.getLocation() : getLocation();
             final String uuid = (String) 
endpoint.getKameletProperties().get(PARAM_UUID);
 
             if (context.getRouteTemplateDefinition(templateId) == null && loc 
!= null) {
diff --git 
a/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletCycleTest.java
 
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletCycleTest.java
new file mode 100644
index 000000000000..813002e92975
--- /dev/null
+++ 
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletCycleTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.kamelet;
+
+import org.apache.camel.CamelContext;
+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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-25387: a Kamelet whose template starts from the Kamelet itself 
created routes until the stack overflowed.
+ */
+public class KameletCycleTest {
+
+    @Test
+    public void aKameletWhoseTemplateStartsFromItself() throws Exception {
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    // from kamelet:loop instead of kamelet:source, as a local 
model wrote it
+                    routeTemplate("loop")
+                            .from("kamelet:loop/route")
+                            .setBody(simple("${body}!"));
+
+                    from("direct:start").to("kamelet:loop");
+                }
+            });
+            Exception e = assertThrows(Exception.class, context::start);
+            String messages = messages(e);
+            assertTrue(messages.contains("Kamelet loop uses itself"), 
messages);
+            assertTrue(messages.contains("starts from kamelet:source"), 
messages);
+        }
+    }
+
+    @Test
+    public void aKameletThatUsesAnotherKameletIsNotACycle() throws Exception {
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    
routeTemplate("outer").from("kamelet:source").to("kamelet:inner");
+                    
routeTemplate("inner").from("kamelet:source").setBody(simple("${body}!"));
+
+                    from("direct:start").to("kamelet:outer");
+                }
+            });
+            context.start();
+            assertEquals("hi!", 
context.createProducerTemplate().requestBody("direct:start", "hi", 
String.class));
+        }
+    }
+
+    @Test
+    public void aKameletUsedTwiceIsNotACycle() throws Exception {
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    
routeTemplate("upper").from("kamelet:source").setBody(simple("${body.toUpperCase()}"));
+
+                    
from("direct:start").to("kamelet:upper").to("kamelet:upper");
+                }
+            });
+            context.start();
+            assertEquals("HI", 
context.createProducerTemplate().requestBody("direct:start", "hi", 
String.class));
+        }
+    }
+
+    private static String messages(Throwable e) {
+        StringBuilder sb = new StringBuilder();
+        for (Throwable t = e; t != null; t = t.getCause()) {
+            sb.append(t.getMessage()).append('\n');
+        }
+        return sb.toString();
+    }
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
index 94601e8b4ca5..9e0ed5cd8778 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
@@ -355,8 +355,9 @@ public class FileWatcherResourceReloadStrategy extends 
ResourceReloadStrategySup
                         }
                     } catch (Throwable e) {
                         // an error (such as ExceptionInInitializerError when 
compiling a route) must not stop watching
+                        // an error such as StackOverflowError has no message: 
name it rather than print null
                         LOG.warn("Error reloading files in directory: {} due 
to: {}. This exception is ignored.", folder,
-                                e.getMessage(), e);
+                                e.getMessage() != null ? e.getMessage() : 
e.getClass().getName(), e);
                     }
                 }
             } finally {

Reply via email to