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 17563be3c63a CAMEL-24301: Fix RouteService NPE when startup-failure 
exception carries no message
17563be3c63a is described below

commit 17563be3c63a2ce67d01573ef5ed7987da0b67ad
Author: mayurbm <[email protected]>
AuthorDate: Tue Aug 4 17:13:05 2026 +0530

    CAMEL-24301: Fix RouteService NPE when startup-failure exception carries 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), getLocalizedMessage() returns null and a 
secondary
    NPE is thrown instead of the intended FailedToStartRouteException.
    
    Adds 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.
    
    Closes #25205
    
    Co-authored-by: Claude <[email protected]>
---
 .../org/apache/camel/impl/engine/RouteService.java |  25 ++-
 .../engine/RouteServiceWarmUpNullMessageTest.java  | 239 +++++++++++++++++++++
 2 files changed, 262 insertions(+), 2 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 246034c73e36..dfc11a66b058 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 class RouteService extends ChildServiceSupport {
         try {
             doWarmUp();
         } catch (Exception e) {
-            throw new FailedToStartRouteException(getId(), getLocation(), 
e.getLocalizedMessage(), e);
+            throw new FailedToStartRouteException(getId(), getLocation(), 
extractUsefulMessage(e), e);
         }
     }
 
@@ -136,11 +136,32 @@ public class RouteService extends ChildServiceSupport {
             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.
+     * <p/>
+     * {@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.
+     */
+    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 000000000000..b0e9e48931bf
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/RouteServiceWarmUpNullMessageTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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.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.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.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * 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}).
+ *
+ * <p>
+ * 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}.
+ *
+ * <p>
+ * 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 the endpoint throws a message-less {@link NullPointerException} 
during route setup, the result must be a
+     * {@link FailedToStartRouteException}, not a raw NPE.
+     */
+    @Test
+    void testSetUpNullMessageExceptionProducesFailedToStartRouteException() {
+        CamelContext context = new DefaultCamelContext();
+        context.addComponent("fail", new NullMessageFailComponent());
+
+        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) {
+            }
+        }
+    }
+
+    /**
+     * The {@link FailedToStartRouteException} message must contain the route 
id and must not use the literal string
+     * "null" as the failure description.
+     */
+    @Test
+    void testFailedToStartMessageIsNonNullAndMeaningful() {
+        CamelContext context = new DefaultCamelContext();
+        context.addComponent("fail", new NullMessageFailComponent());
+
+        try {
+            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) {
+            }
+        }
+    }
+
+    /**
+     * 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 testSetUpWalksCauseChainForMessage() {
+        CamelContext context = new DefaultCamelContext();
+        String expectedFragment = "real cause message from chain";
+        context.addComponent("fail", new 
ChainedNullMessageFailComponent(expectedFragment));
+
+        try {
+            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) {
+            }
+        }
+    }
+
+    // ---- helpers ----
+
+    /**
+     * 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<String, Object> parameters) {
+            return new NullMessageFailEndpoint(uri, this);
+        }
+    }
+
+    private static class NullMessageFailEndpoint extends DefaultEndpoint {
+        NullMessageFailEndpoint(String uri, NullMessageFailComponent 
component) {
+            super(uri, component);
+        }
+
+        @Override
+        protected void doStart() {
+            throw new NullPointerException();
+        }
+
+        @Override
+        public Consumer createConsumer(Processor processor) {
+            return new DefaultConsumer(this, processor) {
+            };
+        }
+
+        @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 in {@code 
extractUsefulMessage}.
+     */
+    private static class ChainedNullMessageFailComponent extends 
DefaultComponent {
+        private final String causeMessage;
+
+        ChainedNullMessageFailComponent(String causeMessage) {
+            this.causeMessage = causeMessage;
+        }
+
+        @Override
+        protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters) {
+            return new ChainedNullMessageFailEndpoint(uri, this, causeMessage);
+        }
+    }
+
+    private static class ChainedNullMessageFailEndpoint extends 
DefaultEndpoint {
+        private final String causeMessage;
+
+        ChainedNullMessageFailEndpoint(String uri, 
ChainedNullMessageFailComponent component, String causeMessage) {
+            super(uri, component);
+            this.causeMessage = causeMessage;
+        }
+
+        @Override
+        protected void doStart() {
+            // 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
+        public Consumer createConsumer(Processor processor) {
+            return new DefaultConsumer(this, processor) {
+            };
+        }
+
+        @Override
+        public Producer createProducer() {
+            return new DefaultProducer(this) {
+                @Override
+                public void process(Exchange exchange) {
+                }
+            };
+        }
+
+        @Override
+        public boolean isSingleton() {
+            return true;
+        }
+    }
+}

Reply via email to