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 8cbbdae6c13b CAMEL-24404: camel-base-engine: wrap consumer startup 
exceptions in FailedToStartRouteException in InternalRouteStartupManager 
(#25554)
8cbbdae6c13b is described below

commit 8cbbdae6c13b72660d4f09a553b99bb3d89ba9d1
Author: mayurbm <[email protected]>
AuthorDate: Fri Aug 28 23:46:49 2026 +0530

    CAMEL-24404: camel-base-engine: wrap consumer startup exceptions in 
FailedToStartRouteException in InternalRouteStartupManager (#25554)
    
    * camel-base-engine: wrap null-message consumer startup exceptions in 
FailedToStartRouteException
    
    Two catch blocks in doStartOrResumeRouteConsumers() re-threw consumer/route-
    service startup exceptions raw. When a consumer's start() throws a 
message-less
    exception (e.g. bare NullPointerException), the null message propagated 
through
    FailedToStartRouteException's constructor (which calls 
Objects.requireNonNull)
    causing a secondary NPE instead of a clean diagnostic error.
    
    Fix: only wrap in FailedToStartRouteException when the exception has a null 
or
    blank message — use RouteService.extractUsefulMessage() to find the first
    meaningful message in the cause chain, falling back to the simple class 
name.
    Exceptions that already carry a message propagate unchanged, preserving 
existing
    behaviour for all callers that assert on the original exception type.
    
    Reuse RouteService.extractUsefulMessage() (package-private static, same 
package)
    instead of duplicating the logic.
    
    Co-authored-by: Claude <[email protected]>
    
    * camel-base-engine: use AssertJ in test, add coverage for 
routeService.start() catch site
    
    Switch test assertions from JUnit to AssertJ assertThatThrownBy pattern per
    project conventions. Add a third test covering the second catch site
    (routeService.start()) using doStart() override to trigger the null-message
    wrapping path through BaseService.
    
    Co-authored-by: Claude <[email protected]>
    
    * camel-base-engine: wrap consumer startup exceptions unconditionally in 
FailedToStartRouteException
    
    Per davsclaus review: remove the null/blank-message condition from both
    catch blocks in InternalRouteStartupManager.doStartOrResumeRouteConsumers()
    so every consumer and route-service start failure is always wrapped in
    FailedToStartRouteException carrying route ID and location context.
    
    Update all affected tests across 7 modules to reflect the new exception
    wrapping — asserting FailedToStartRouteException as the outer type and
    checking the original exception via getCause():
    - DefaultSupervisingRouteControllerTest (camel-core)
    - MainSupervisingRouteControllerTest (camel-main)
    - SpringSupervisingRouteControllerTest (camel-spring-xml)
    - NettyHttpTwoRoutesValidateBootstrapConfigurationTest (camel-netty-http)
    - JmsTestConnectionOnStartupTest (camel-jms)
    - MyBatisUnknownStatementTypeTest (camel-mybatis)
    - AiToolEndpointLifecycleTest (camel-ai-tool)
    
    All modified tests verified locally with JDK 21.
    
    * camel-base-engine: add RoutePolicy.onStart test genuinely exercising 
second catch site
    
    The previous 
testRouteServiceStartNullMessageProducesFailedToStartRouteException
    test used doStart() on the consumer, which was still caught by the first 
catch
    site (camelContext.startService(consumer)), not the second 
(routeService.start()).
    
    Add testRoutePolicyOnStartProducesFailedToStartRouteException: a RoutePolicy
    whose onStart() throws is called from DefaultRoute.doStart() inside
    routeService.start(), which is the genuine second catch site. The consumer
    starts successfully so the failure cannot be intercepted by the first block.
    
    * camel-base-engine: fix misleading Javadoc on 
RouteServiceStartFailComponent test
    
    testRouteServiceStartNullMessageProducesFailedToStartRouteException and
    RouteServiceStartFailComponent were documented as exercising the second 
catch
    site (routeService.start()) but actually hit the first catch site
    (camelContext.startService(consumer)) via BaseService.start() -> doStart().
    
    Correct the Javadoc to accurately describe what each test covers.
    The testRoutePolicyOnStartProducesFailedToStartRouteException test (added in
    the previous commit) is the one that genuinely exercises the second catch 
site.
    
    * camel-base-engine: wrap null-message consumer startup exceptions in 
FailedToStartRouteException
    
    Two catch blocks in doStartOrResumeRouteConsumers() re-threw consumer/route-
    service startup exceptions raw. When a consumer's start() throws a 
message-less
    exception (e.g. bare NullPointerException), the null message propagated 
through
    FailedToStartRouteException's constructor (which calls 
Objects.requireNonNull)
    causing a secondary NPE instead of a clean diagnostic error.
    
    Fix: only wrap in FailedToStartRouteException when the exception has a null 
or
    blank message — use RouteService.extractUsefulMessage() to find the first
    meaningful message in the cause chain, falling back to the simple class 
name.
    Exceptions that already carry a message propagate unchanged, preserving 
existing
    behaviour for all callers that assert on the original exception type.
    
    Reuse RouteService.extractUsefulMessage() (package-private static, same 
package)
    instead of duplicating the logic.
    
    Co-authored-by: Claude <[email protected]>
    
    * camel-base-engine: use AssertJ in test, add coverage for 
routeService.start() catch site
    
    Switch test assertions from JUnit to AssertJ assertThatThrownBy pattern per
    project conventions. Add a third test covering the second catch site
    (routeService.start()) using doStart() override to trigger the null-message
    wrapping path through BaseService.
    
    Co-authored-by: Claude <[email protected]>
    
    * camel-base-engine: wrap consumer startup exceptions unconditionally in 
FailedToStartRouteException
    
    Per davsclaus review: remove the null/blank-message condition from both
    catch blocks in InternalRouteStartupManager.doStartOrResumeRouteConsumers()
    so every consumer and route-service start failure is always wrapped in
    FailedToStartRouteException carrying route ID and location context.
    
    Update all affected tests across 7 modules to reflect the new exception
    wrapping — asserting FailedToStartRouteException as the outer type and
    checking the original exception via getCause():
    - DefaultSupervisingRouteControllerTest (camel-core)
    - MainSupervisingRouteControllerTest (camel-main)
    - SpringSupervisingRouteControllerTest (camel-spring-xml)
    - NettyHttpTwoRoutesValidateBootstrapConfigurationTest (camel-netty-http)
    - JmsTestConnectionOnStartupTest (camel-jms)
    - MyBatisUnknownStatementTypeTest (camel-mybatis)
    - AiToolEndpointLifecycleTest (camel-ai-tool)
    
    All modified tests verified locally with JDK 21.
    
    * camel-base-engine: add RoutePolicy.onStart test genuinely exercising 
second catch site
    
    The previous 
testRouteServiceStartNullMessageProducesFailedToStartRouteException
    test used doStart() on the consumer, which was still caught by the first 
catch
    site (camelContext.startService(consumer)), not the second 
(routeService.start()).
    
    Add testRoutePolicyOnStartProducesFailedToStartRouteException: a RoutePolicy
    whose onStart() throws is called from DefaultRoute.doStart() inside
    routeService.start(), which is the genuine second catch site. The consumer
    starts successfully so the failure cannot be intercepted by the first block.
    
    * camel-base-engine: fix misleading Javadoc on 
RouteServiceStartFailComponent test
    
    testRouteServiceStartNullMessageProducesFailedToStartRouteException and
    RouteServiceStartFailComponent were documented as exercising the second 
catch
    site (routeService.start()) but actually hit the first catch site
    (camelContext.startService(consumer)) via BaseService.start() -> doStart().
    
    Correct the Javadoc to accurately describe what each test covers.
    The testRoutePolicyOnStartProducesFailedToStartRouteException test (added in
    the previous commit) is the one that genuinely exercises the second catch 
site.
    
    ---------
    
    Co-authored-by: mayurmohan <[email protected]>
    Co-authored-by: Claude <[email protected]>
---
 .../ai/tool/AiToolEndpointLifecycleTest.java       |   7 +-
 .../jms/JmsTestConnectionOnStartupTest.java        |   4 +-
 .../mybatis/MyBatisUnknownStatementTypeTest.java   |   3 +-
 ...woRoutesValidateBootstrapConfigurationTest.java |   8 +-
 .../impl/SpringSupervisingRouteControllerTest.java |   8 +-
 .../impl/engine/InternalRouteStartupManager.java   |   8 +-
 .../DefaultSupervisingRouteControllerTest.java     |   9 +-
 ...ternalRouteStartupManagerConsumerStartTest.java | 262 +++++++++++++++++++++
 .../main/MainSupervisingRouteControllerTest.java   |  11 +-
 9 files changed, 301 insertions(+), 19 deletions(-)

diff --git 
a/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolEndpointLifecycleTest.java
 
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolEndpointLifecycleTest.java
index 0238416bc352..d2b3fa130008 100644
--- 
a/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolEndpointLifecycleTest.java
+++ 
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolEndpointLifecycleTest.java
@@ -18,6 +18,7 @@ package org.apache.camel.component.ai.tool;
 
 import java.util.Set;
 
+import org.apache.camel.FailedToStartRouteException;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.test.junit6.CamelTestSupport;
 import org.junit.jupiter.api.Test;
@@ -352,7 +353,8 @@ public class AiToolEndpointLifecycleTest extends 
CamelTestSupport {
                      + "&argSchema={\"type\":\"object\"}")
                         .setBody(constant("invalid"));
             }
-        })).isInstanceOf(IllegalArgumentException.class)
+        })).isInstanceOf(FailedToStartRouteException.class)
+                .cause().isInstanceOf(IllegalArgumentException.class)
                 .hasMessageContaining("mutually exclusive");
     }
 
@@ -432,7 +434,8 @@ public class AiToolEndpointLifecycleTest extends 
CamelTestSupport {
                      + "&outputSchema={\"type\":\"object\"}")
                         .setBody(constant("invalid"));
             }
-        })).isInstanceOf(IllegalArgumentException.class)
+        })).isInstanceOf(FailedToStartRouteException.class)
+                .cause().isInstanceOf(IllegalArgumentException.class)
                 .hasMessageContaining("mutually exclusive");
     }
 
diff --git 
a/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsTestConnectionOnStartupTest.java
 
b/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsTestConnectionOnStartupTest.java
index bd3c041adfec..8a804cd529f7 100644
--- 
a/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsTestConnectionOnStartupTest.java
+++ 
b/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsTestConnectionOnStartupTest.java
@@ -21,6 +21,7 @@ import jakarta.jms.ConnectionFactory;
 import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
 import org.apache.camel.CamelContext;
 import org.apache.camel.FailedToCreateProducerException;
+import org.apache.camel.FailedToStartRouteException;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.test.junit6.CamelTestSupport;
 import org.junit.jupiter.api.Test;
@@ -46,10 +47,11 @@ public class JmsTestConnectionOnStartupTest extends 
CamelTestSupport {
             context.start();
             fail("Should have thrown an exception");
         } catch (Exception e) {
+            assertIsInstanceOf(FailedToStartRouteException.class, e);
             assertEquals(
                     "Failed to create Consumer for endpoint: 
activemq://queue:JmsTestConnectionOnStartupTest?testConnectionOnStartup=true. "
                          + "Reason: Cannot get JMS Connection on startup for 
destination JmsTestConnectionOnStartupTest",
-                    e.getMessage());
+                    e.getCause().getMessage());
         }
     }
 
diff --git 
a/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisUnknownStatementTypeTest.java
 
b/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisUnknownStatementTypeTest.java
index 4bb8f3c92a26..99d12f288c71 100644
--- 
a/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisUnknownStatementTypeTest.java
+++ 
b/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisUnknownStatementTypeTest.java
@@ -42,7 +42,8 @@ public class MyBatisUnknownStatementTypeTest extends 
CamelTestSupport {
         Exception e = assertThrows(Exception.class,
                 () -> context.start());
         assertIsInstanceOf(IllegalArgumentException.class, 
e.getCause().getCause());
-        assertEquals("statementType must be specified on: 
mybatis://selectAllAccounts", e.getCause().getCause().getMessage());
+        assertEquals("statementType must be specified on: 
mybatis://selectAllAccounts",
+                e.getCause().getCause().getMessage());
     }
 
 }
diff --git 
a/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpTwoRoutesValidateBootstrapConfigurationTest.java
 
b/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpTwoRoutesValidateBootstrapConfigurationTest.java
index 11b1fd5b4b70..7de86aaf5a44 100644
--- 
a/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpTwoRoutesValidateBootstrapConfigurationTest.java
+++ 
b/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpTwoRoutesValidateBootstrapConfigurationTest.java
@@ -16,9 +16,11 @@
  */
 package org.apache.camel.component.netty.http;
 
+import org.apache.camel.FailedToStartRouteException;
 import org.apache.camel.builder.RouteBuilder;
 import org.junit.jupiter.api.Test;
 
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -44,10 +46,12 @@ public class 
NettyHttpTwoRoutesValidateBootstrapConfigurationTest extends BaseNe
                         .transform().constant("Bye Camel");
             }
         });
-        IllegalArgumentException e = 
assertThrows(IllegalArgumentException.class,
+        FailedToStartRouteException e = 
assertThrows(FailedToStartRouteException.class,
                 () -> context.start(),
                 "Should have thrown exception");
-        assertTrue(e.getMessage().startsWith("Bootstrap configuration must be 
identical when adding additional consumer"));
+        assertInstanceOf(IllegalArgumentException.class, e.getCause());
+        assertTrue(e.getCause().getMessage()
+                .startsWith("Bootstrap configuration must be identical when 
adding additional consumer"));
     }
 
 }
diff --git 
a/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/impl/SpringSupervisingRouteControllerTest.java
 
b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/impl/SpringSupervisingRouteControllerTest.java
index e41ad66b41ac..4cb436e24d9e 100644
--- 
a/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/impl/SpringSupervisingRouteControllerTest.java
+++ 
b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/impl/SpringSupervisingRouteControllerTest.java
@@ -21,6 +21,7 @@ import java.util.concurrent.TimeUnit;
 
 import org.apache.camel.Consumer;
 import org.apache.camel.Endpoint;
+import org.apache.camel.FailedToStartRouteException;
 import org.apache.camel.Processor;
 import org.apache.camel.component.mock.MockEndpoint;
 import org.apache.camel.component.seda.SedaComponent;
@@ -33,8 +34,8 @@ import 
org.springframework.context.support.AbstractXmlApplicationContext;
 import org.springframework.context.support.ClassPathXmlApplicationContext;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class SpringSupervisingRouteControllerTest extends SpringTestSupport {
 
@@ -69,8 +70,9 @@ public class SpringSupervisingRouteControllerTest extends 
SpringTestSupport {
 
         Throwable e = src.getRestartException("cake");
         assertNotNull(e);
-        assertEquals("Cannot start", e.getMessage());
-        assertTrue(e instanceof IllegalArgumentException);
+        assertInstanceOf(FailedToStartRouteException.class, e);
+        assertInstanceOf(IllegalArgumentException.class, e.getCause());
+        assertEquals("Cannot start", e.getCause().getMessage());
 
         // bar is no auto startup
         assertEquals("Stopped", 
context.getRouteController().getRouteStatus("bar").toString());
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/InternalRouteStartupManager.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/InternalRouteStartupManager.java
index 4e8efa7fc614..8eafc028ffc8 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/InternalRouteStartupManager.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/InternalRouteStartupManager.java
@@ -426,7 +426,9 @@ final class InternalRouteStartupManager {
                         route.getProperties().remove("route.start.exception");
                     } catch (Exception e) {
                         route.getProperties().put("route.start.exception", e);
-                        throw e;
+                        throw new FailedToStartRouteException(
+                                routeService.getId(), 
routeService.getLocation(),
+                                RouteService.extractUsefulMessage(e), e);
                     }
 
                     // use basic endpoint uri to not log verbose details or 
potential sensitive data
@@ -464,7 +466,9 @@ final class InternalRouteStartupManager {
                     route.getProperties().remove("route.start.exception");
                 } catch (Exception e) {
                     route.getProperties().put("route.start.exception", e);
-                    throw e;
+                    throw new FailedToStartRouteException(
+                            routeService.getId(), routeService.getLocation(),
+                            RouteService.extractUsefulMessage(e), e);
                 }
             }
 
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerTest.java
index 54f9c8c5c39e..f01cc75f748c 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerTest.java
@@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit;
 import org.apache.camel.Consumer;
 import org.apache.camel.ContextTestSupport;
 import org.apache.camel.Endpoint;
+import org.apache.camel.FailedToStartRouteException;
 import org.apache.camel.Processor;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.mock.MockEndpoint;
@@ -42,8 +43,8 @@ import org.junit.jupiter.api.condition.DisabledOnOs;
 
 import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 @DisabledOnOs(architectures = { "s390x" },
               disabledReason = "This test does not run reliably on s390x (see 
CAMEL-21438)")
@@ -106,9 +107,9 @@ public class DefaultSupervisingRouteControllerTest extends 
ContextTestSupport {
                 .atMost(Duration.ofMillis(src.getInitialDelay() + 
src.getBackOffDelay() * (src.getBackOffMaxAttempts() + 1)))
                 .untilAsserted(() -> 
assertNotNull(src.getRestartException("cake")));
         Throwable e = src.getRestartException("cake");
-        assertEquals("Cannot start", e.getMessage());
-        boolean b = e instanceof IllegalArgumentException;
-        assertTrue(b);
+        assertInstanceOf(FailedToStartRouteException.class, e);
+        assertInstanceOf(IllegalArgumentException.class, e.getCause());
+        assertEquals("Cannot start", e.getCause().getMessage());
 
         // bar is no auto startup
         assertEquals("Stopped", 
context.getRouteController().getRouteStatus("bar").toString());
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/engine/InternalRouteStartupManagerConsumerStartTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/InternalRouteStartupManagerConsumerStartTest.java
new file mode 100644
index 000000000000..df87082f7371
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/InternalRouteStartupManagerConsumerStartTest.java
@@ -0,0 +1,262 @@
+/*
+ * 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.impl.engine;
+
+import java.util.Map;
+
+import org.apache.camel.Consumer;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Exchange;
+import org.apache.camel.FailedToStartRouteException;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.Route;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.RoutePolicy;
+import org.apache.camel.support.DefaultComponent;
+import org.apache.camel.support.DefaultConsumer;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.DefaultProducer;
+import org.apache.camel.support.RoutePolicySupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Verifies that {@link InternalRouteStartupManager} wraps consumer startup 
failures in a
+ * {@link FailedToStartRouteException} with a meaningful (non-null) message, 
even when the root cause carries no message
+ * (e.g. a bare {@link NullPointerException}).
+ *
+ * <p>
+ * Before the fix, the two catch sites in {@code 
doStartOrResumeRouteConsumers()} re-threw the raw exception without any
+ * wrapping, causing a bare NPE to escape directly to the caller instead of a 
proper
+ * {@link FailedToStartRouteException}.
+ */
+class InternalRouteStartupManagerConsumerStartTest {
+
+    /**
+     * A bare {@link NullPointerException} (no message) thrown from the 
consumer's {@code start()} must be wrapped in a
+     * {@link FailedToStartRouteException} with a non-null message containing 
the route id.
+     */
+    @Test
+    void testConsumerStartNullMessageProducesFailedToStartRouteException() 
throws Exception {
+        DefaultCamelContext context = new DefaultCamelContext();
+        context.addComponent("failstart", new ConsumerStartFailComponent(new 
NullPointerException()));
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("failstart:trigger").routeId("consumer-start-route").to("direct:out");
+            }
+        });
+
+        assertThatThrownBy(() -> context.start())
+                .isInstanceOf(FailedToStartRouteException.class)
+                .hasMessageContaining("consumer-start-route")
+                .hasMessageNotContaining("because: null");
+
+        context.stop();
+    }
+
+    /**
+     * When the consumer startup exception has no message but a cause does, 
the cause message must be surfaced in the
+     * {@link FailedToStartRouteException} rather than falling back to a 
generic class name.
+     */
+    @Test
+    void testConsumerStartWalksCauseChainForMessage() throws Exception {
+        String expectedFragment = "real cause from consumer start";
+        RuntimeException chainedException = new RuntimeException((String) 
null, new IllegalStateException(expectedFragment));
+
+        DefaultCamelContext context = new DefaultCamelContext();
+        context.addComponent("failstart", new 
ConsumerStartFailComponent(chainedException));
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("failstart:trigger").routeId("consumer-chain-route").to("direct:out");
+            }
+        });
+
+        assertThatThrownBy(() -> context.start())
+                .isInstanceOf(FailedToStartRouteException.class)
+                .hasMessageContaining(expectedFragment);
+
+        context.stop();
+    }
+
+    /**
+     * A bare {@link NullPointerException} thrown from the consumer's {@code 
doStart()} (as opposed to {@code start()})
+     * must also be wrapped in a {@link FailedToStartRouteException}. This 
exercises the first catch site via
+     * {@code camelContext.startService(consumer)}, reaching the consumer 
through {@code BaseService.start()} →
+     * {@code doStart()}.
+     */
+    @Test
+    void testRouteServiceStartNullMessageProducesFailedToStartRouteException() 
throws Exception {
+        DefaultCamelContext context = new DefaultCamelContext();
+        context.addComponent("failstart", new 
RouteServiceStartFailComponent(new NullPointerException()));
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("failstart:trigger").routeId("route-service-start-route").to("direct:out");
+            }
+        });
+
+        assertThatThrownBy(() -> context.start())
+                .isInstanceOf(FailedToStartRouteException.class)
+                .hasMessageContaining("route-service-start-route")
+                .hasMessageNotContaining("because: null");
+
+        context.stop();
+    }
+
+    /**
+     * A {@link RoutePolicy#onStart} failure exercises the second catch site 
in {@code doStartOrResumeRouteConsumers()}
+     * via {@code routeService.start()}, which calls {@code 
DefaultRoute.doStart()} →
+     * {@code routePolicyCallback(RoutePolicy::onStart)}. The consumer starts 
successfully; only the route-level policy
+     * callback throws, so this cannot be caught by the first catch site.
+     */
+    @Test
+    void testRoutePolicyOnStartProducesFailedToStartRouteException() throws 
Exception {
+        RuntimeException cause = new NullPointerException();
+
+        DefaultCamelContext context = new DefaultCamelContext();
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:trigger").routeId("policy-fail-route")
+                        .routePolicy(new RoutePolicySupport() {
+                            @Override
+                            public void onStart(Route route) {
+                                throw cause;
+                            }
+                        })
+                        .to("mock:out");
+            }
+        });
+
+        assertThatThrownBy(() -> context.start())
+                .isInstanceOf(FailedToStartRouteException.class)
+                .hasMessageContaining("policy-fail-route")
+                .hasMessageNotContaining("because: null");
+
+        context.stop();
+    }
+
+    // ---- helpers ----
+
+    /**
+     * Component whose consumer throws from {@link Consumer#start()}, 
exercising the first catch site in
+     * {@code doStartOrResumeRouteConsumers()}.
+     */
+    private static class ConsumerStartFailComponent extends DefaultComponent {
+        private final RuntimeException toThrow;
+
+        ConsumerStartFailComponent(RuntimeException toThrow) {
+            this.toThrow = toThrow;
+        }
+
+        @Override
+        protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters) {
+            return new FailStartEndpoint(uri, this, toThrow);
+        }
+
+        private static class FailStartEndpoint extends DefaultEndpoint {
+            private final RuntimeException toThrow;
+
+            FailStartEndpoint(String uri, ConsumerStartFailComponent 
component, RuntimeException toThrow) {
+                super(uri, component);
+                this.toThrow = toThrow;
+            }
+
+            @Override
+            public Consumer createConsumer(Processor processor) {
+                return new DefaultConsumer(this, processor) {
+                    @Override
+                    public void start() {
+                        throw toThrow;
+                    }
+                };
+            }
+
+            @Override
+            public Producer createProducer() {
+                return new DefaultProducer(this) {
+                    @Override
+                    public void process(Exchange exchange) {
+                    }
+                };
+            }
+
+            @Override
+            public boolean isSingleton() {
+                return true;
+            }
+        }
+    }
+
+    /**
+     * Component whose consumer throws from {@link 
org.apache.camel.support.service.BaseService#doStart()}, still
+     * exercising the first catch site in {@code 
doStartOrResumeRouteConsumers()} via
+     * {@code camelContext.startService(consumer)} → {@code 
BaseService.start()} → {@code doStart()}. Covers the
+     * {@code doStart()} override path as distinct from the {@code start()} 
override in
+     * {@link ConsumerStartFailComponent}.
+     */
+    private static class RouteServiceStartFailComponent extends 
DefaultComponent {
+        private final RuntimeException toThrow;
+
+        RouteServiceStartFailComponent(RuntimeException toThrow) {
+            this.toThrow = toThrow;
+        }
+
+        @Override
+        protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters) {
+            return new FailStartEndpoint(uri, this, toThrow);
+        }
+
+        private static class FailStartEndpoint extends DefaultEndpoint {
+            private final RuntimeException toThrow;
+
+            FailStartEndpoint(String uri, RouteServiceStartFailComponent 
component, RuntimeException toThrow) {
+                super(uri, component);
+                this.toThrow = toThrow;
+            }
+
+            @Override
+            public Consumer createConsumer(Processor processor) {
+                return new DefaultConsumer(this, processor) {
+                    @Override
+                    protected void doStart() {
+                        throw toThrow;
+                    }
+                };
+            }
+
+            @Override
+            public Producer createProducer() {
+                return new DefaultProducer(this) {
+                    @Override
+                    public void process(Exchange exchange) {
+                    }
+                };
+            }
+
+            @Override
+            public boolean isSingleton() {
+                return true;
+            }
+        }
+    }
+}
diff --git 
a/core/camel-main/src/test/java/org/apache/camel/main/MainSupervisingRouteControllerTest.java
 
b/core/camel-main/src/test/java/org/apache/camel/main/MainSupervisingRouteControllerTest.java
index 7cf9f4234ae5..d1c75a1a99b2 100644
--- 
a/core/camel-main/src/test/java/org/apache/camel/main/MainSupervisingRouteControllerTest.java
+++ 
b/core/camel-main/src/test/java/org/apache/camel/main/MainSupervisingRouteControllerTest.java
@@ -21,6 +21,7 @@ import java.util.concurrent.TimeUnit;
 
 import org.apache.camel.Consumer;
 import org.apache.camel.Endpoint;
+import org.apache.camel.FailedToStartRouteException;
 import org.apache.camel.Processor;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.mock.MockEndpoint;
@@ -73,8 +74,9 @@ public class MainSupervisingRouteControllerTest {
         SupervisingRouteController src = (SupervisingRouteController) 
main.camelContext.getRouteController();
         Throwable e = src.getRestartException("cake");
         assertNotNull(e);
-        assertEquals("Cannot start", e.getMessage());
-        assertInstanceOf(IllegalArgumentException.class, e);
+        assertInstanceOf(FailedToStartRouteException.class, e);
+        assertInstanceOf(IllegalArgumentException.class, e.getCause());
+        assertEquals("Cannot start", e.getCause().getMessage());
 
         // bar is no auto startup
         assertEquals("Stopped", 
main.camelContext.getRouteController().getRouteStatus("bar").toString());
@@ -150,8 +152,9 @@ public class MainSupervisingRouteControllerTest {
         SupervisingRouteController src = (SupervisingRouteController) 
main.camelContext.getRouteController();
         Throwable e = src.getRestartException("cake");
         assertNotNull(e);
-        assertEquals("Cannot start", e.getMessage());
-        assertInstanceOf(IllegalArgumentException.class, e);
+        assertInstanceOf(FailedToStartRouteException.class, e);
+        assertInstanceOf(IllegalArgumentException.class, e.getCause());
+        assertEquals("Cannot start", e.getCause().getMessage());
 
         // bar is no auto startup
         assertEquals("Stopped", 
main.camelContext.getRouteController().getRouteStatus("bar").toString());

Reply via email to