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());