From d489daf35b998dd76c957148363b096139684e9c Mon Sep 17 00:00:00 2001 From: mayurmohan Date: Wed, 29 Jul 2026 11:05:53 +0530 Subject: [PATCH 1/5] Fix RouteService to avoid NullPointerException when startup failure has no message RouteService.warmUp() and setUp() passed e.getLocalizedMessage() directly to FailedToStartRouteException, whose constructor calls Objects.requireNonNull on the message argument. When the root cause is a message-less exception (e.g. a bare NullPointerException, StackOverflowError, or a wrapped WSDLException with null description), getLocalizedMessage() returns null and a secondary NPE is thrown from inside the FailedToStartRouteException constructor instead of the intended FailedToStartRouteException. The fix introduces a private extractUsefulMessage helper that walks the cause chain to find the first non-null, non-blank message, falling back to the exception's simple class name. This guarantees the constructor always receives a non-null message and the caller always sees a FailedToStartRouteException with a meaningful description. Adds RouteServiceWarmUpNullMessageTest covering: - message-less NullPointerException on warm-up (regression guard) - message never contains "because: null" - cause-chain walking surfaces the real nested message --- .../camel/impl/engine/RouteService.java | 26 +- .../RouteServiceWarmUpNullMessageTest.java | 249 ++++++++++++++++++ 2 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java index 246034c73e36b..5a5b726eb4c44 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java @@ -127,7 +127,7 @@ public void warmUp() throws FailedToStartRouteException { try { doWarmUp(); } catch (Exception e) { - throw new FailedToStartRouteException(getId(), getLocation(), e.getLocalizedMessage(), e); + throw new FailedToStartRouteException(getId(), getLocation(), extractUsefulMessage(e), e); } } @@ -136,11 +136,33 @@ public void setUp() throws FailedToStartRouteException { try { doSetup(); } catch (Exception e) { - throw new FailedToStartRouteException(getId(), getLocation(), e.getLocalizedMessage(), e); + throw new FailedToStartRouteException(getId(), getLocation(), extractUsefulMessage(e), e); } } } + /** + * Extracts a non-null, non-empty error message from the exception or its cause chain. + *

+ * {@link Throwable#getLocalizedMessage()} can return {@code null} for exceptions such as + * {@link NullPointerException} that carry no message, which would cause + * {@link FailedToStartRouteException} to throw {@link NullPointerException} from its own constructor + * (via {@code Objects.requireNonNull}) instead of wrapping the original failure. This helper walks the + * cause chain to find the first meaningful message and falls back to the simple class name so the + * caller always receives a non-null string. + */ + private static String extractUsefulMessage(Throwable e) { + Throwable current = e; + while (current != null) { + String msg = current.getLocalizedMessage(); + if (msg != null && !msg.isBlank()) { + return msg; + } + current = current.getCause(); + } + return e.getClass().getSimpleName(); + } + public boolean isAutoStartup() { if (!getCamelContext().isAutoStartup()) { return false; diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java new file mode 100644 index 0000000000000..e068b926451b8 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java @@ -0,0 +1,249 @@ +/* + * 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 org.apache.camel.CamelContext; +import org.apache.camel.FailedToStartRouteException; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.direct.DirectComponent; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.support.DefaultComponent; +import org.apache.camel.support.DefaultEndpoint; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that {@link RouteService#warmUp()} wraps startup failures in a {@link FailedToStartRouteException} + * whose message is always meaningful — even when the root cause exception itself carries a {@code null} message + * (e.g. a bare {@link NullPointerException}). + * + *

Before the fix, {@code RouteService} passed {@code e.getLocalizedMessage()} directly to the + * {@link FailedToStartRouteException} constructor, which calls {@code Objects.requireNonNull} on that + * argument. A message-less exception therefore caused a secondary {@link NullPointerException} to be thrown + * from inside the exception constructor rather than a proper {@link FailedToStartRouteException}. + */ +public class RouteServiceWarmUpNullMessageTest { + + /** + * When a route's consumer throws a {@link NullPointerException} with no message during warm-up, the + * resulting {@link FailedToStartRouteException} must still carry a non-null, non-empty message. + */ + @Test + public void testWarmUpNullMessageExceptionProducesUsefulFailedToStartMessage() { + CamelContext context = new DefaultCamelContext(); + // Register a component whose endpoint start throws a message-less NullPointerException + context.addComponent("fail", new NullMessageFailComponent()); + + assertThrows(FailedToStartRouteException.class, () -> { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("fail:trigger").routeId("test-route").to("direct:out"); + } + }); + context.start(); + }); + } + + /** + * The {@link FailedToStartRouteException} message must not be null, must contain the route id, and must + * not use the literal string "null" as the failure description. + */ + @Test + public void testFailedToStartMessageIsNonNullAndMeaningful() { + CamelContext context = new DefaultCamelContext(); + context.addComponent("fail", new NullMessageFailComponent()); + + FailedToStartRouteException caught = null; + try { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("fail:trigger").routeId("meaningful-route").to("direct:out"); + } + }); + context.start(); + } catch (FailedToStartRouteException e) { + caught = e; + } catch (Exception e) { + Throwable t = e; + while (t != null) { + if (t instanceof FailedToStartRouteException) { + caught = (FailedToStartRouteException) t; + break; + } + t = t.getCause(); + } + } finally { + try { + context.stop(); + } catch (Exception ignored) { + } + } + + assertNotNull(caught, "Expected FailedToStartRouteException"); + String message = caught.getMessage(); + assertNotNull(message, "FailedToStartRouteException message must not be null"); + assertTrue(message.contains("meaningful-route"), "Message must contain the route id"); + // The key regression guard: must NOT say "because: null" + assertFalse(message.contains("because: null"), + "Message must not contain 'because: null' — was: " + message); + // The fallback must surface something useful (class name at minimum) + assertTrue(message.length() > "Failed to start route: meaningful-route because: ".length(), + "Message must have a non-empty failure description — was: " + message); + } + + /** + * Verifies that when the root exception has a null message but its cause has a real message, + * the cause's message is surfaced in the {@link FailedToStartRouteException}. + */ + @Test + public void testWarmUpWalksCauseChainForMessage() { + CamelContext context = new DefaultCamelContext(); + String expectedFragment = "real cause message from chain"; + context.addComponent("fail", new ChainedNullMessageFailComponent(expectedFragment)); + + FailedToStartRouteException caught = null; + try { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("fail:trigger").routeId("chain-route").to("direct:out"); + } + }); + context.start(); + } catch (FailedToStartRouteException e) { + caught = e; + } catch (Exception e) { + Throwable t = e; + while (t != null) { + if (t instanceof FailedToStartRouteException) { + caught = (FailedToStartRouteException) t; + break; + } + t = t.getCause(); + } + } finally { + try { + context.stop(); + } catch (Exception ignored) { + } + } + + assertNotNull(caught, "Expected FailedToStartRouteException"); + assertTrue(caught.getMessage().contains(expectedFragment), + "Message should surface cause chain message — was: " + caught.getMessage()); + } + + // ---- helpers ---- + + /** A component whose endpoint throws a message-less {@link NullPointerException} on start. */ + private static class NullMessageFailComponent extends DefaultComponent { + @Override + protected org.apache.camel.Endpoint createEndpoint(String uri, String remaining, + java.util.Map parameters) { + return new FailOnStartEndpoint(uri, this); + } + + private static class FailOnStartEndpoint extends DefaultEndpoint { + FailOnStartEndpoint(String uri, NullMessageFailComponent component) { + super(uri, component); + } + + @Override + public org.apache.camel.Consumer createConsumer(org.apache.camel.Processor processor) { + return new org.apache.camel.support.DefaultConsumer(this, processor) { + @Override + protected void doStart() { + // Throw a NullPointerException with no message — the classic null-message case + throw new NullPointerException(); + } + }; + } + + @Override + public org.apache.camel.Producer createProducer() { + return new org.apache.camel.support.DefaultProducer(this) { + @Override + public void process(org.apache.camel.Exchange exchange) { + } + }; + } + + @Override + public boolean isSingleton() { + return true; + } + } + } + + /** + * A component whose endpoint throws a message-less outer exception wrapping an inner exception that + * does have a message — used to test cause-chain walking. + */ + private static class ChainedNullMessageFailComponent extends DefaultComponent { + private final String causeMessage; + + ChainedNullMessageFailComponent(String causeMessage) { + this.causeMessage = causeMessage; + } + + @Override + protected org.apache.camel.Endpoint createEndpoint(String uri, String remaining, + java.util.Map parameters) { + return new FailOnStartEndpoint(uri, this, causeMessage); + } + + private static class FailOnStartEndpoint extends DefaultEndpoint { + private final String causeMessage; + + FailOnStartEndpoint(String uri, ChainedNullMessageFailComponent component, String causeMessage) { + super(uri, component); + this.causeMessage = causeMessage; + } + + @Override + public org.apache.camel.Consumer createConsumer(org.apache.camel.Processor processor) { + return new org.apache.camel.support.DefaultConsumer(this, processor) { + @Override + protected void doStart() { + // Outer exception has no message; inner cause has the real message + throw new RuntimeException(new IllegalStateException(causeMessage)); + } + }; + } + + @Override + public org.apache.camel.Producer createProducer() { + return new org.apache.camel.support.DefaultProducer(this) { + @Override + public void process(org.apache.camel.Exchange exchange) { + } + }; + } + + @Override + public boolean isSingleton() { + return true; + } + } + } +} From eb06378b1647fa288835175d9b3ade133207d5fd Mon Sep 17 00:00:00 2001 From: mayurmohan Date: Mon, 3 Aug 2026 12:21:05 +0530 Subject: [PATCH 2/5] Apply formatting and code-style fixes to RouteService fix - RouteService.java: fix Javadoc

to

per Camel convention, reflow comment lines via Eclipse formatter - RouteServiceWarmUpNullMessageTest.java: replace JUnit assertions with AssertJ, drop public from class/methods (JUnit 5 convention), replace FQCNs with proper imports, reformat via formatter:format + impsort:sort --- .../camel/impl/engine/RouteService.java | 11 +- .../RouteServiceWarmUpNullMessageTest.java | 158 +++++++++--------- 2 files changed, 84 insertions(+), 85 deletions(-) diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java index 5a5b726eb4c44..58ce5ff5076e2 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java @@ -143,13 +143,12 @@ public void setUp() throws FailedToStartRouteException { /** * Extracts a non-null, non-empty error message from the exception or its cause chain. - *

+ *

* {@link Throwable#getLocalizedMessage()} can return {@code null} for exceptions such as - * {@link NullPointerException} that carry no message, which would cause - * {@link FailedToStartRouteException} to throw {@link NullPointerException} from its own constructor - * (via {@code Objects.requireNonNull}) instead of wrapping the original failure. This helper walks the - * cause chain to find the first meaningful message and falls back to the simple class name so the - * caller always receives a non-null string. + * {@link NullPointerException} that carry no message, which would cause {@link FailedToStartRouteException} to + * throw {@link NullPointerException} from its own constructor (via {@code Objects.requireNonNull}) instead of + * wrapping the original failure. This helper walks the cause chain to find the first meaningful message and falls + * back to the simple class name so the caller always receives a non-null string. */ private static String extractUsefulMessage(Throwable e) { Throwable current = e; diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java index e068b926451b8..73c18fc25b6ac 100644 --- a/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java @@ -16,78 +16,83 @@ */ package org.apache.camel.impl.engine; +import java.util.Map; + import org.apache.camel.CamelContext; +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.builder.RouteBuilder; -import org.apache.camel.component.direct.DirectComponent; import org.apache.camel.impl.DefaultCamelContext; 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.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** - * Verifies that {@link RouteService#warmUp()} wraps startup failures in a {@link FailedToStartRouteException} - * whose message is always meaningful — even when the root cause exception itself carries a {@code null} message - * (e.g. a bare {@link NullPointerException}). + * Verifies that {@link RouteService#warmUp()} wraps startup failures in a {@link FailedToStartRouteException} whose + * message is always meaningful — even when the root cause exception itself carries a {@code null} message (e.g. a bare + * {@link NullPointerException}). * - *

Before the fix, {@code RouteService} passed {@code e.getLocalizedMessage()} directly to the - * {@link FailedToStartRouteException} constructor, which calls {@code Objects.requireNonNull} on that - * argument. A message-less exception therefore caused a secondary {@link NullPointerException} to be thrown - * from inside the exception constructor rather than a proper {@link FailedToStartRouteException}. + *

+ * Before the fix, {@code RouteService} passed {@code e.getLocalizedMessage()} directly to the + * {@link FailedToStartRouteException} constructor, which calls {@code Objects.requireNonNull} on that argument. A + * message-less exception therefore caused a secondary {@link NullPointerException} to be thrown from inside the + * exception constructor rather than a proper {@link FailedToStartRouteException}. */ -public class RouteServiceWarmUpNullMessageTest { +class RouteServiceWarmUpNullMessageTest { /** - * When a route's consumer throws a {@link NullPointerException} with no message during warm-up, the - * resulting {@link FailedToStartRouteException} must still carry a non-null, non-empty message. + * When a route's consumer throws a {@link NullPointerException} with no message during warm-up, the resulting + * {@link FailedToStartRouteException} must still carry a non-null, non-empty message. */ @Test - public void testWarmUpNullMessageExceptionProducesUsefulFailedToStartMessage() { + void testWarmUpNullMessageExceptionProducesUsefulFailedToStartMessage() throws Exception { CamelContext context = new DefaultCamelContext(); - // Register a component whose endpoint start throws a message-less NullPointerException context.addComponent("fail", new NullMessageFailComponent()); - - assertThrows(FailedToStartRouteException.class, () -> { - context.addRoutes(new RouteBuilder() { - @Override - public void configure() { - from("fail:trigger").routeId("test-route").to("direct:out"); - } - }); - context.start(); + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("fail:trigger").routeId("test-route").to("direct:out"); + } }); + + assertThatThrownBy(context::start) + .isInstanceOf(FailedToStartRouteException.class); } /** - * The {@link FailedToStartRouteException} message must not be null, must contain the route id, and must - * not use the literal string "null" as the failure description. + * The {@link FailedToStartRouteException} message must not be null, must contain the route id, and must not use the + * literal string "null" as the failure description. */ @Test - public void testFailedToStartMessageIsNonNullAndMeaningful() { + void testFailedToStartMessageIsNonNullAndMeaningful() throws Exception { CamelContext context = new DefaultCamelContext(); context.addComponent("fail", new NullMessageFailComponent()); + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("fail:trigger").routeId("meaningful-route").to("direct:out"); + } + }); FailedToStartRouteException caught = null; try { - context.addRoutes(new RouteBuilder() { - @Override - public void configure() { - from("fail:trigger").routeId("meaningful-route").to("direct:out"); - } - }); context.start(); } catch (FailedToStartRouteException e) { caught = e; } catch (Exception e) { Throwable t = e; while (t != null) { - if (t instanceof FailedToStartRouteException) { - caught = (FailedToStartRouteException) t; + if (t instanceof FailedToStartRouteException ftsre) { + caught = ftsre; break; } t = t.getCause(); @@ -99,44 +104,42 @@ public void configure() { } } - assertNotNull(caught, "Expected FailedToStartRouteException"); - String message = caught.getMessage(); - assertNotNull(message, "FailedToStartRouteException message must not be null"); - assertTrue(message.contains("meaningful-route"), "Message must contain the route id"); - // The key regression guard: must NOT say "because: null" - assertFalse(message.contains("because: null"), - "Message must not contain 'because: null' — was: " + message); - // The fallback must surface something useful (class name at minimum) - assertTrue(message.length() > "Failed to start route: meaningful-route because: ".length(), - "Message must have a non-empty failure description — was: " + message); + assertThat(caught).as("Expected FailedToStartRouteException").isNotNull(); + assertThat(caught.getMessage()) + .as("FailedToStartRouteException message must not be null") + .isNotNull() + .as("Message must contain the route id") + .contains("meaningful-route") + .as("Message must not contain 'because: null'") + .doesNotContain("because: null"); } /** - * Verifies that when the root exception has a null message but its cause has a real message, - * the cause's message is surfaced in the {@link FailedToStartRouteException}. + * Verifies that when the root exception has a null message but its cause has a real message, the cause's message is + * surfaced in the {@link FailedToStartRouteException}. */ @Test - public void testWarmUpWalksCauseChainForMessage() { + void testWarmUpWalksCauseChainForMessage() throws Exception { CamelContext context = new DefaultCamelContext(); String expectedFragment = "real cause message from chain"; context.addComponent("fail", new ChainedNullMessageFailComponent(expectedFragment)); + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("fail:trigger").routeId("chain-route").to("direct:out"); + } + }); FailedToStartRouteException caught = null; try { - context.addRoutes(new RouteBuilder() { - @Override - public void configure() { - from("fail:trigger").routeId("chain-route").to("direct:out"); - } - }); context.start(); } catch (FailedToStartRouteException e) { caught = e; } catch (Exception e) { Throwable t = e; while (t != null) { - if (t instanceof FailedToStartRouteException) { - caught = (FailedToStartRouteException) t; + if (t instanceof FailedToStartRouteException ftsre) { + caught = ftsre; break; } t = t.getCause(); @@ -148,9 +151,10 @@ public void configure() { } } - assertNotNull(caught, "Expected FailedToStartRouteException"); - assertTrue(caught.getMessage().contains(expectedFragment), - "Message should surface cause chain message — was: " + caught.getMessage()); + assertThat(caught).as("Expected FailedToStartRouteException").isNotNull(); + assertThat(caught.getMessage()) + .as("Message should surface cause chain message") + .contains(expectedFragment); } // ---- helpers ---- @@ -158,8 +162,7 @@ public void configure() { /** A component whose endpoint throws a message-less {@link NullPointerException} on start. */ private static class NullMessageFailComponent extends DefaultComponent { @Override - protected org.apache.camel.Endpoint createEndpoint(String uri, String remaining, - java.util.Map parameters) { + protected Endpoint createEndpoint(String uri, String remaining, Map parameters) { return new FailOnStartEndpoint(uri, this); } @@ -169,21 +172,20 @@ private static class FailOnStartEndpoint extends DefaultEndpoint { } @Override - public org.apache.camel.Consumer createConsumer(org.apache.camel.Processor processor) { - return new org.apache.camel.support.DefaultConsumer(this, processor) { + public Consumer createConsumer(Processor processor) { + return new DefaultConsumer(this, processor) { @Override protected void doStart() { - // Throw a NullPointerException with no message — the classic null-message case throw new NullPointerException(); } }; } @Override - public org.apache.camel.Producer createProducer() { - return new org.apache.camel.support.DefaultProducer(this) { + public Producer createProducer() { + return new DefaultProducer(this) { @Override - public void process(org.apache.camel.Exchange exchange) { + public void process(Exchange exchange) { } }; } @@ -196,8 +198,8 @@ public boolean isSingleton() { } /** - * A component whose endpoint throws a message-less outer exception wrapping an inner exception that - * does have a message — used to test cause-chain walking. + * A component whose endpoint throws a message-less outer exception wrapping an inner exception that does have a + * message — used to test cause-chain walking. */ private static class ChainedNullMessageFailComponent extends DefaultComponent { private final String causeMessage; @@ -207,8 +209,7 @@ private static class ChainedNullMessageFailComponent extends DefaultComponent { } @Override - protected org.apache.camel.Endpoint createEndpoint(String uri, String remaining, - java.util.Map parameters) { + protected Endpoint createEndpoint(String uri, String remaining, Map parameters) { return new FailOnStartEndpoint(uri, this, causeMessage); } @@ -221,21 +222,20 @@ private static class FailOnStartEndpoint extends DefaultEndpoint { } @Override - public org.apache.camel.Consumer createConsumer(org.apache.camel.Processor processor) { - return new org.apache.camel.support.DefaultConsumer(this, processor) { + public Consumer createConsumer(Processor processor) { + return new DefaultConsumer(this, processor) { @Override protected void doStart() { - // Outer exception has no message; inner cause has the real message throw new RuntimeException(new IllegalStateException(causeMessage)); } }; } @Override - public org.apache.camel.Producer createProducer() { - return new org.apache.camel.support.DefaultProducer(this) { + public Producer createProducer() { + return new DefaultProducer(this) { @Override - public void process(org.apache.camel.Exchange exchange) { + public void process(Exchange exchange) { } }; } From 3b0d444cb7bf1764925ddf81a7ff6c3bbc3c43cb Mon Sep 17 00:00:00 2001 From: mayurmohan Date: Mon, 3 Aug 2026 12:41:02 +0530 Subject: [PATCH 3/5] Fix test to exercise RouteService.setUp() path, not consumer start The consumer doStart() is called by InternalRouteStartupManager, not by RouteService.warmUp()/setUp(). Move the failure into the endpoint's own doStart() so it is triggered via ServiceHelper.initService(endpoint) inside RouteService.doSetup(), which is the code path the fix actually covers. Also rename test methods to reflect setUp() rather than warmUp(). --- .../RouteServiceWarmUpNullMessageTest.java | 187 +++++++++--------- 1 file changed, 99 insertions(+), 88 deletions(-) diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java index 73c18fc25b6ac..ac24df9785520 100644 --- a/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java @@ -37,54 +37,59 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; /** - * Verifies that {@link RouteService#warmUp()} wraps startup failures in a {@link FailedToStartRouteException} whose - * message is always meaningful — even when the root cause exception itself carries a {@code null} message (e.g. a bare - * {@link NullPointerException}). + * Verifies that {@link RouteService#warmUp()} and {@link RouteService#setUp()} wrap startup failures in a + * {@link FailedToStartRouteException} whose message is always meaningful — even when the root cause exception carries a + * {@code null} message (e.g. a bare {@link NullPointerException}). * *

* Before the fix, {@code RouteService} passed {@code e.getLocalizedMessage()} directly to the * {@link FailedToStartRouteException} constructor, which calls {@code Objects.requireNonNull} on that argument. A * message-less exception therefore caused a secondary {@link NullPointerException} to be thrown from inside the * exception constructor rather than a proper {@link FailedToStartRouteException}. + * + *

+ * The tests trigger the failure during endpoint initialisation (inside {@code doSetup()}), which is the code path + * covered by the {@code RouteService} fix. */ class RouteServiceWarmUpNullMessageTest { /** - * When a route's consumer throws a {@link NullPointerException} with no message during warm-up, the resulting - * {@link FailedToStartRouteException} must still carry a non-null, non-empty message. + * When the endpoint throws a message-less {@link NullPointerException} during route setup, the result must be a + * {@link FailedToStartRouteException}, not a raw NPE. */ @Test - void testWarmUpNullMessageExceptionProducesUsefulFailedToStartMessage() throws Exception { + void testSetUpNullMessageExceptionProducesFailedToStartRouteException() { CamelContext context = new DefaultCamelContext(); context.addComponent("fail", new NullMessageFailComponent()); - context.addRoutes(new RouteBuilder() { - @Override - public void configure() { - from("fail:trigger").routeId("test-route").to("direct:out"); - } - }); - assertThatThrownBy(context::start) - .isInstanceOf(FailedToStartRouteException.class); + assertThatThrownBy(() -> { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("fail:trigger").routeId("test-route").to("direct:out"); + } + }); + context.start(); + }).isInstanceOf(FailedToStartRouteException.class); } /** - * The {@link FailedToStartRouteException} message must not be null, must contain the route id, and must not use the - * literal string "null" as the failure description. + * The {@link FailedToStartRouteException} message must contain the route id and must not use the literal string + * "null" as the failure description. */ @Test - void testFailedToStartMessageIsNonNullAndMeaningful() throws Exception { + void testFailedToStartMessageIsNonNullAndMeaningful() { CamelContext context = new DefaultCamelContext(); context.addComponent("fail", new NullMessageFailComponent()); - context.addRoutes(new RouteBuilder() { - @Override - public void configure() { - from("fail:trigger").routeId("meaningful-route").to("direct:out"); - } - }); FailedToStartRouteException caught = null; try { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("fail:trigger").routeId("meaningful-route").to("direct:out"); + } + }); context.start(); } catch (FailedToStartRouteException e) { caught = e; @@ -115,23 +120,23 @@ public void configure() { } /** - * Verifies that when the root exception has a null message but its cause has a real message, the cause's message is - * surfaced in the {@link FailedToStartRouteException}. + * When the endpoint throws a message-less outer exception wrapping an inner exception that has a message, the inner + * message must be surfaced in the {@link FailedToStartRouteException}. */ @Test - void testWarmUpWalksCauseChainForMessage() throws Exception { + void testSetUpWalksCauseChainForMessage() { CamelContext context = new DefaultCamelContext(); String expectedFragment = "real cause message from chain"; context.addComponent("fail", new ChainedNullMessageFailComponent(expectedFragment)); - context.addRoutes(new RouteBuilder() { - @Override - public void configure() { - from("fail:trigger").routeId("chain-route").to("direct:out"); - } - }); FailedToStartRouteException caught = null; try { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("fail:trigger").routeId("chain-route").to("direct:out"); + } + }); context.start(); } catch (FailedToStartRouteException e) { caught = e; @@ -159,47 +164,52 @@ public void configure() { // ---- helpers ---- - /** A component whose endpoint throws a message-less {@link NullPointerException} on start. */ + /** + * A component whose endpoint throws a message-less {@link NullPointerException} during its own {@code doStart()} — + * which is invoked by {@code RouteService.doSetup()} via {@code ServiceHelper.initService(endpoint)}, exercising + * the {@code setUp()} fix. + */ private static class NullMessageFailComponent extends DefaultComponent { @Override protected Endpoint createEndpoint(String uri, String remaining, Map parameters) { - return new FailOnStartEndpoint(uri, this); + return new NullMessageFailEndpoint(uri, this); } + } - private static class FailOnStartEndpoint extends DefaultEndpoint { - FailOnStartEndpoint(String uri, NullMessageFailComponent component) { - super(uri, component); - } + private static class NullMessageFailEndpoint extends DefaultEndpoint { + NullMessageFailEndpoint(String uri, NullMessageFailComponent component) { + super(uri, component); + } - @Override - public Consumer createConsumer(Processor processor) { - return new DefaultConsumer(this, processor) { - @Override - protected void doStart() { - throw new NullPointerException(); - } - }; - } + @Override + protected void doStart() { + throw new NullPointerException(); + } - @Override - public Producer createProducer() { - return new DefaultProducer(this) { - @Override - public void process(Exchange exchange) { - } - }; - } + @Override + public Consumer createConsumer(Processor processor) { + return new DefaultConsumer(this, processor) { + }; + } - @Override - public boolean isSingleton() { - return true; - } + @Override + public Producer createProducer() { + return new DefaultProducer(this) { + @Override + public void process(Exchange exchange) { + } + }; + } + + @Override + public boolean isSingleton() { + return true; } } /** * A component whose endpoint throws a message-less outer exception wrapping an inner exception that does have a - * message — used to test cause-chain walking. + * message — used to test cause-chain walking in {@code extractUsefulMessage}. */ private static class ChainedNullMessageFailComponent extends DefaultComponent { private final String causeMessage; @@ -210,40 +220,41 @@ private static class ChainedNullMessageFailComponent extends DefaultComponent { @Override protected Endpoint createEndpoint(String uri, String remaining, Map parameters) { - return new FailOnStartEndpoint(uri, this, causeMessage); + return new ChainedNullMessageFailEndpoint(uri, this, causeMessage); } + } - private static class FailOnStartEndpoint extends DefaultEndpoint { - private final String causeMessage; + private static class ChainedNullMessageFailEndpoint extends DefaultEndpoint { + private final String causeMessage; - FailOnStartEndpoint(String uri, ChainedNullMessageFailComponent component, String causeMessage) { - super(uri, component); - this.causeMessage = causeMessage; - } + ChainedNullMessageFailEndpoint(String uri, ChainedNullMessageFailComponent component, String causeMessage) { + super(uri, component); + this.causeMessage = causeMessage; + } - @Override - public Consumer createConsumer(Processor processor) { - return new DefaultConsumer(this, processor) { - @Override - protected void doStart() { - throw new RuntimeException(new IllegalStateException(causeMessage)); - } - }; - } + @Override + protected void doStart() { + throw new RuntimeException(new IllegalStateException(causeMessage)); + } - @Override - public Producer createProducer() { - return new DefaultProducer(this) { - @Override - public void process(Exchange exchange) { - } - }; - } + @Override + public Consumer createConsumer(Processor processor) { + return new DefaultConsumer(this, processor) { + }; + } - @Override - public boolean isSingleton() { - return true; - } + @Override + public Producer createProducer() { + return new DefaultProducer(this) { + @Override + public void process(Exchange exchange) { + } + }; + } + + @Override + public boolean isSingleton() { + return true; } } } From 16a1e50eed249d2778eceeb5e682fb3a4fa17b4a Mon Sep 17 00:00:00 2001 From: mayurmohan Date: Mon, 3 Aug 2026 12:57:08 +0530 Subject: [PATCH 4/5] Fix cause-chain test to use NPE with initCause instead of RuntimeException(cause) RuntimeException(Throwable) sets detailMessage to cause.toString() which is non-null, so extractUsefulMessage() returned on the first iteration without ever walking the chain. Using a bare NullPointerException with initCause() ensures the outer exception has a null message, forcing the helper to walk to the cause to find the real message. --- .../impl/engine/RouteServiceWarmUpNullMessageTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java index ac24df9785520..9ec78dccd3068 100644 --- a/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java @@ -234,7 +234,11 @@ private static class ChainedNullMessageFailEndpoint extends DefaultEndpoint { @Override protected void doStart() { - throw new RuntimeException(new IllegalStateException(causeMessage)); + // Outer NPE has no message; initCause sets the cause without supplying a message to + // the outer exception — forces extractUsefulMessage to walk the chain to find causeMessage. + NullPointerException outer = new NullPointerException(); + outer.initCause(new IllegalStateException(causeMessage)); + throw outer; } @Override From e1c398b064d2a6bd3ad20702127a6436290aa271 Mon Sep 17 00:00:00 2001 From: mayurmohan Date: Tue, 4 Aug 2026 13:05:35 +0530 Subject: [PATCH 5/5] Address davsclaus review: package-private helper, consistent test style - Make extractUsefulMessage package-private (drop private modifier) so DefaultSupervisingRouteController can reuse it for the same null-safety fix at its own call site - Wrap test 1 in try/finally to call context.stop() on cleanup, matching the pattern already used in tests 2 and 3 - Refactor tests 2 and 3 to use assertThatThrownBy + fluent assertion chaining, consistent with test 1 and removing manual catch/unwrap boilerplate - Remove now-unused assertThat static import --- .../camel/impl/engine/RouteService.java | 2 +- .../RouteServiceWarmUpNullMessageTest.java | 99 +++++++------------ 2 files changed, 38 insertions(+), 63 deletions(-) diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java index 58ce5ff5076e2..dfc11a66b0585 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java @@ -150,7 +150,7 @@ public void setUp() throws FailedToStartRouteException { * wrapping the original failure. This helper walks the cause chain to find the first meaningful message and falls * back to the simple class name so the caller always receives a non-null string. */ - private static String extractUsefulMessage(Throwable e) { + static String extractUsefulMessage(Throwable e) { Throwable current = e; while (current != null) { String msg = current.getLocalizedMessage(); diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java index 9ec78dccd3068..b0e9e48931bfc 100644 --- a/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java @@ -33,7 +33,6 @@ import org.apache.camel.support.DefaultProducer; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** @@ -62,15 +61,22 @@ void testSetUpNullMessageExceptionProducesFailedToStartRouteException() { CamelContext context = new DefaultCamelContext(); context.addComponent("fail", new NullMessageFailComponent()); - assertThatThrownBy(() -> { - context.addRoutes(new RouteBuilder() { - @Override - public void configure() { - from("fail:trigger").routeId("test-route").to("direct:out"); - } - }); - context.start(); - }).isInstanceOf(FailedToStartRouteException.class); + try { + assertThatThrownBy(() -> { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("fail:trigger").routeId("test-route").to("direct:out"); + } + }); + context.start(); + }).isInstanceOf(FailedToStartRouteException.class); + } finally { + try { + context.stop(); + } catch (Exception ignored) { + } + } } /** @@ -82,41 +88,24 @@ void testFailedToStartMessageIsNonNullAndMeaningful() { CamelContext context = new DefaultCamelContext(); context.addComponent("fail", new NullMessageFailComponent()); - FailedToStartRouteException caught = null; try { - context.addRoutes(new RouteBuilder() { - @Override - public void configure() { - from("fail:trigger").routeId("meaningful-route").to("direct:out"); - } - }); - context.start(); - } catch (FailedToStartRouteException e) { - caught = e; - } catch (Exception e) { - Throwable t = e; - while (t != null) { - if (t instanceof FailedToStartRouteException ftsre) { - caught = ftsre; - break; - } - t = t.getCause(); - } + assertThatThrownBy(() -> { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("fail:trigger").routeId("meaningful-route").to("direct:out"); + } + }); + context.start(); + }).isInstanceOf(FailedToStartRouteException.class) + .hasMessageContaining("meaningful-route") + .hasMessageNotContaining("because: null"); } finally { try { context.stop(); } catch (Exception ignored) { } } - - assertThat(caught).as("Expected FailedToStartRouteException").isNotNull(); - assertThat(caught.getMessage()) - .as("FailedToStartRouteException message must not be null") - .isNotNull() - .as("Message must contain the route id") - .contains("meaningful-route") - .as("Message must not contain 'because: null'") - .doesNotContain("because: null"); } /** @@ -129,37 +118,23 @@ void testSetUpWalksCauseChainForMessage() { String expectedFragment = "real cause message from chain"; context.addComponent("fail", new ChainedNullMessageFailComponent(expectedFragment)); - FailedToStartRouteException caught = null; try { - context.addRoutes(new RouteBuilder() { - @Override - public void configure() { - from("fail:trigger").routeId("chain-route").to("direct:out"); - } - }); - context.start(); - } catch (FailedToStartRouteException e) { - caught = e; - } catch (Exception e) { - Throwable t = e; - while (t != null) { - if (t instanceof FailedToStartRouteException ftsre) { - caught = ftsre; - break; - } - t = t.getCause(); - } + assertThatThrownBy(() -> { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("fail:trigger").routeId("chain-route").to("direct:out"); + } + }); + context.start(); + }).isInstanceOf(FailedToStartRouteException.class) + .hasMessageContaining(expectedFragment); } finally { try { context.stop(); } catch (Exception ignored) { } } - - assertThat(caught).as("Expected FailedToStartRouteException").isNotNull(); - assertThat(caught.getMessage()) - .as("Message should surface cause chain message") - .contains(expectedFragment); } // ---- helpers ----