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 32d935f59ae6 CAMEL-26460: camel-undertow - a static path next to a 
path template: the right route for the base path, and stopping the template 
route keeps it (#29039)
32d935f59ae6 is described below

commit 32d935f59ae6fc161c79737c4efcdb99d2dc8607
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Nov 3 14:17:05 2026 +0530

    CAMEL-26460: camel-undertow - a static path next to a path template: the 
right route for the base path, and stopping the template route keeps it (#29039)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../undertow/handlers/CamelPathHandler.java        |  46 ++++-
 .../handlers/CamelPathTemplateHandler.java         |  42 ++++-
 .../undertow/handlers/CamelRootHandler.java        |  49 ++++-
 .../UndertowPrefixPathAfterTemplatePathTest.java   |  70 ++++++++
 .../UndertowTemplatePathAndStaticPathTest.java     | 197 +++++++++++++++++++++
 .../undertow/handlers/CamelRootHandlerTest.java    |  74 ++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  14 ++
 7 files changed, 471 insertions(+), 21 deletions(-)

diff --git 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelPathHandler.java
 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelPathHandler.java
index c2ccebfa8bbf..7834ff65f9df 100644
--- 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelPathHandler.java
+++ 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelPathHandler.java
@@ -30,7 +30,10 @@ import io.undertow.util.URLUtils;
  */
 public class CamelPathHandler extends PathHandler {
     private final Lock lock = new ReentrantLock();
-    private final Map<String, HttpHandler> handlers = new HashMap<>();
+    // the exact and the prefix paths are kept apart, as a base path can have 
both: a static path, and the path
+    // templates under it (a prefix path)
+    private final Map<String, HttpHandler> exactHandlers = new HashMap<>();
+    private final Map<String, HttpHandler> prefixHandlers = new HashMap<>();
     private final HttpHandler defaultHandler;
     private String handlerString;
 
@@ -44,7 +47,7 @@ public class CamelPathHandler extends PathHandler {
         lock.lock();
         try {
             super.addPrefixPath(path, handler);
-            handlers.put(path, handler);
+            prefixHandlers.put(path, handler);
             handlerString = null;
             return this;
         } finally {
@@ -57,7 +60,7 @@ public class CamelPathHandler extends PathHandler {
         lock.lock();
         try {
             super.addExactPath(path, handler);
-            handlers.put(path, handler);
+            exactHandlers.put(path, handler);
             handlerString = null;
             return this;
         } finally {
@@ -75,7 +78,7 @@ public class CamelPathHandler extends PathHandler {
                 // every request that no other path matches would fail: use 
the default handler again
                 super.addPrefixPath("/", defaultHandler);
             }
-            handlers.remove(path);
+            prefixHandlers.remove(path);
             handlerString = null;
             return this;
         } finally {
@@ -88,7 +91,7 @@ public class CamelPathHandler extends PathHandler {
         lock.lock();
         try {
             super.removeExactPath(path);
-            handlers.remove(path);
+            exactHandlers.remove(path);
             handlerString = null;
             return this;
         } finally {
@@ -96,18 +99,45 @@ public class CamelPathHandler extends PathHandler {
         }
     }
 
+    /**
+     * The handler of the path: its prefix handler (such as the handler of the 
path templates under it), or else its
+     * exact handler.
+     */
     public HttpHandler getHandler(String path) {
-        return handlers.get(path);
+        lock.lock();
+        try {
+            HttpHandler handler = prefixHandlers.get(path);
+            return handler != null ? handler : exactHandlers.get(path);
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * The exact handler of the path, also when the path has a prefix handler 
too.
+     */
+    public HttpHandler getExactHandler(String path) {
+        lock.lock();
+        try {
+            return exactHandlers.get(path);
+        } finally {
+            lock.unlock();
+        }
     }
 
     public boolean isEmpty() {
-        return handlers.isEmpty();
+        lock.lock();
+        try {
+            return exactHandlers.isEmpty() && prefixHandlers.isEmpty();
+        } finally {
+            lock.unlock();
+        }
     }
 
     @Override
     public String toString() {
         if (handlerString == null) {
-            handlerString = "CamelPathHandler[" + handlers + "]";
+            handlerString = "CamelPathHandler[exact=" + exactHandlers + ", 
prefix=" + prefixHandlers + "]";
         }
         return handlerString;
     }
diff --git 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelPathTemplateHandler.java
 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelPathTemplateHandler.java
index b8cd757546b4..d00dc5d93894 100644
--- 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelPathTemplateHandler.java
+++ 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelPathTemplateHandler.java
@@ -53,6 +53,17 @@ public class CamelPathTemplateHandler implements HttpHandler 
{
 
     @Override
     public void handleRequest(HttpServerExchange exchange) throws Exception {
+        String relativePath = exchange.getRelativePath();
+        if ("".equals(relativePath) || "/".equals(relativePath)) {
+            CamelMethodHandler staticHandler = defaultHandlerWrapper.get();
+            if (!staticHandler.isEmpty()) {
+                // a request for the base path itself (also with a trailing 
slash, and the root path, which Undertow
+                // serves through its default handler with the whole path as 
the rest) goes to the static path of the
+                // base path: Undertow would match its rest against a template 
such as /{id}, with an empty value
+                staticHandler.handleRequest(exchange);
+                return;
+            }
+        }
         delegate.handleRequest(exchange);
     }
 
@@ -107,6 +118,19 @@ public class CamelPathTemplateHandler implements 
HttpHandler {
         }
     }
 
+    /**
+     * @param exact whether the static path of the base path is registered as 
an exact path (no matchOnUriPrefix): it
+     *              then answers only the base path, and a deeper path that no 
template matches gets the fallback
+     */
+    public void setDefaultExact(boolean exact) {
+        lock.lock();
+        try {
+            this.defaultHandlerWrapper.setExact(exact);
+        } finally {
+            lock.unlock();
+        }
+    }
+
     @Override
     public String toString() {
         if (handlerString == null) {
@@ -122,6 +146,11 @@ public class CamelPathTemplateHandler implements 
HttpHandler {
          * or null to let {@link #handler} answer 405. It is set once, when 
the template handler is created.
          */
         private HttpHandler fallback;
+        /**
+         * Whether {@link #handler} is the handler of an exact path, which 
answers the base path only (Undertow gives it
+         * the base path before this prefix path).
+         */
+        private volatile boolean exact;
 
         public void set(CamelMethodHandler handler) {
             this.handler = handler;
@@ -131,6 +160,10 @@ public class CamelPathTemplateHandler implements 
HttpHandler {
             this.fallback = fallback;
         }
 
+        public void setExact(boolean exact) {
+            this.exact = exact;
+        }
+
         public CamelMethodHandler get() {
             return this.handler;
         }
@@ -139,10 +172,11 @@ public class CamelPathTemplateHandler implements 
HttpHandler {
         public void handleRequest(HttpServerExchange exchange) throws 
Exception {
             CamelMethodHandler current = handler;
             HttpHandler other = fallback;
-            if (other != null && current.isEmpty()) {
-                // no template and no static path of this base path matches: 
the request is not for this base path,
-                // so it gets the answer of a request that no path of the 
server matches (404, or 405 when a rest-dsl
-                // consumer of the port has the path with another method)
+            if (other != null && (exact || current.isEmpty())) {
+                // no template and no static path of this base path matches 
(an exact static path answers only the
+                // base path itself): the request is not for this base path, 
so it gets the answer of a request that no
+                // path of the server matches (404, or 405 when a rest-dsl 
consumer of the port has the path with
+                // another method)
                 other.handleRequest(exchange);
             } else {
                 current.handleRequest(exchange);
diff --git 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelRootHandler.java
 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelRootHandler.java
index 48af8dd59b43..f263d64806ac 100644
--- 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelRootHandler.java
+++ 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelRootHandler.java
@@ -70,8 +70,16 @@ public class CamelRootHandler implements HttpHandler {
                 // Adding a handler for the static path
                 if (basePathHandler instanceof CamelPathTemplateHandler) {
                     CamelPathTemplateHandler templateHandler = 
(CamelPathTemplateHandler) basePathHandler;
-                    if (!prefixMatch) {
+                    if (!prefixMatch || 
templateHandler.getDefault().isEmpty()) {
+                        // the static path of the base path; a prefix path 
(matchOnUriPrefix) only when the base path
+                        // has no static path yet, as when it comes before the 
templates
                         targetHandler = templateHandler.getDefault();
+                        if (!prefixMatch && targetHandler.isEmpty() && 
pathHandler.getExactHandler(basePath) == null) {
+                            // the path templates came first: register the 
static path as an exact path too, as when it
+                            // comes first, so that it stays served when the 
templates are removed
+                            pathHandler.addExactPath(basePath, targetHandler);
+                            templateHandler.setDefaultExact(true);
+                        }
                     } else {
                         throw new 
IllegalArgumentException(String.format("Duplicate handlers on a path '%s'", 
path));
                     }
@@ -103,6 +111,7 @@ public class CamelRootHandler implements HttpHandler {
         if (basePathHandler instanceof CamelMethodHandler) {
             // A static path handler is already set for the base path. Use it 
as a default handler
             templateHandler = new 
CamelPathTemplateHandler((CamelMethodHandler) basePathHandler, defaultHandler);
+            
templateHandler.setDefaultExact(pathHandler.getExactHandler(basePath) == 
basePathHandler);
         } else if (basePathHandler == null) {
             // a request under the base path that no template matches goes to 
the default handler of the server, as
             // when the base path is not there
@@ -127,24 +136,30 @@ public class CamelRootHandler implements HttpHandler {
 
             if (path.contains("{")) {
                 // Removing a handler for the template path
+                if (!(basePathHandler instanceof CamelPathTemplateHandler 
templateHandler)) {
+                    // no template is registered under the base path
+                    return;
+                }
                 String relativePath = path.substring(basePath.length());
-                CamelPathTemplateHandler templateHandler = 
(CamelPathTemplateHandler) basePathHandler;
                 CamelMethodHandler targetHandler = 
templateHandler.get(relativePath);
-                if (targetHandler.remove(methods)) {
+                if (targetHandler != null && targetHandler.remove(methods)) {
                     templateHandler.remove(relativePath);
                     if (templateHandler.isEmpty()) {
-                        pathHandler.removePrefixPath(basePath);
+                        removeTemplateHandler(basePath, templateHandler);
                     }
                 }
 
             } else {
                 // Removing a handler for the static path
                 if (basePathHandler instanceof CamelPathTemplateHandler) {
-                    String relativePath = path.substring(basePath.length());
                     CamelPathTemplateHandler templateHandler = 
(CamelPathTemplateHandler) basePathHandler;
                     CamelMethodHandler targetHandler = 
templateHandler.getDefault();
                     if (targetHandler.remove(methods)) {
-                        templateHandler.remove(relativePath);
+                        // the templates stay; an exact registration of the 
static path goes
+                        if (pathHandler.getExactHandler(basePath) == 
targetHandler) {
+                            pathHandler.removeExactPath(basePath);
+                            templateHandler.setDefaultExact(false);
+                        }
                         if (templateHandler.isEmpty()) {
                             pathHandler.removePrefixPath(basePath);
                         }
@@ -152,10 +167,12 @@ public class CamelRootHandler implements HttpHandler {
                 } else {
                     CamelMethodHandler targetHandler = (CamelMethodHandler) 
basePathHandler;
                     if (targetHandler.remove(methods)) {
-                        if (prefixMatch) {
-                            pathHandler.removePrefixPath(basePath);
-                        } else {
+                        // remove the registration that this handler has, 
whichever matching the route that is removed
+                        // asks for (routes of both kinds on one path share 
the handler of the first one)
+                        if (pathHandler.getExactHandler(basePath) == 
targetHandler) {
                             pathHandler.removeExactPath(basePath);
+                        } else {
+                            pathHandler.removePrefixPath(basePath);
                         }
                     }
                 }
@@ -165,6 +182,20 @@ public class CamelRootHandler implements HttpHandler {
         }
     }
 
+    /**
+     * Removes the handler of the path templates under the base path, when it 
has no template left. A static path of the
+     * base path that it served stays: an exact path stays registered as an 
exact path, and a prefix path
+     * (matchOnUriPrefix) is registered as the prefix path again.
+     */
+    private void removeTemplateHandler(String basePath, 
CamelPathTemplateHandler templateHandler) {
+        CamelMethodHandler staticHandler = templateHandler.getDefault();
+        if (staticHandler.isEmpty() || pathHandler.getExactHandler(basePath) 
== staticHandler) {
+            pathHandler.removePrefixPath(basePath);
+        } else {
+            pathHandler.addPrefixPath(basePath, staticHandler);
+        }
+    }
+
     public boolean isEmpty() {
         lock.lock();
         try {
diff --git 
a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowPrefixPathAfterTemplatePathTest.java
 
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowPrefixPathAfterTemplatePathTest.java
new file mode 100644
index 000000000000..b0b259d3c355
--- /dev/null
+++ 
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowPrefixPathAfterTemplatePathTest.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.undertow;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A matchOnUriPrefix route of a base path declared after the route of a path 
template under it: it starts (it was
+ * refused with "Duplicate handlers on a path", while the other order worked), 
answers the base path and what no
+ * template matches, and starts again while the template route runs.
+ */
+class UndertowPrefixPathAfterTemplatePathTest extends BaseUndertowTest {
+
+    @Test
+    void prefixRouteDeclaredAfterTheTemplateRoute() throws Exception {
+        // the template route starts first: the matchOnUriPrefix route of its 
base path starts too (it was refused)
+        assertResponse(getPort(), "/goods", 200, "goods");
+        assertResponse(getPort(), "/goods/a/b", 200, "goods");
+        assertResponse(getPort(), "/goods/7", 200, "good 7");
+
+        // and starts again while the template route runs
+        context.getRouteController().stopRoute("goods");
+        assertResponse(getPort(), "/goods/a/b", 404, null);
+        context.getRouteController().startRoute("goods");
+        assertResponse(getPort(), "/goods/a/b", 200, "goods");
+
+        context.getRouteController().stopRoute("good");
+        assertResponse(getPort(), "/goods/7", 200, "goods");
+        assertResponse(getPort(), "/goods", 200, "goods");
+    }
+
+    private void assertResponse(int port, String path, int code, String body) {
+        Exchange out = template.request("undertow:http://localhost:"; + port + 
path + "?throwExceptionOnFailure=false", null);
+        assertEquals(code, 
out.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class), path);
+        if (body != null) {
+            assertEquals(body, out.getMessage().getBody(String.class), path);
+        }
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                // the template path starts first, then the prefix path
+                
from("undertow:http://localhost:{{port}}/goods/{id}";).routeId("good")
+                        .setBody(simple("good ${header.id}"));
+                
from("undertow:http://localhost:{{port}}/goods?matchOnUriPrefix=true";).routeId("goods")
+                        .setBody(constant("goods"));
+            }
+        };
+    }
+}
diff --git 
a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowTemplatePathAndStaticPathTest.java
 
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowTemplatePathAndStaticPathTest.java
new file mode 100644
index 000000000000..ee833084bc63
--- /dev/null
+++ 
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowTemplatePathAndStaticPathTest.java
@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.undertow;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A path template (such as /orders/{id}) and a static path of the same base 
path (/orders, exact or with
+ * matchOnUriPrefix), served by different routes on one server: the static 
path answers the base path itself, and
+ * stopping the route of the template leaves the static path served (and the 
server running), whichever of the two
+ * routes started first.
+ */
+class UndertowTemplatePathAndStaticPathTest extends BaseUndertowTest {
+
+    @RegisterExtension
+    AvailablePortFinder.Port port3 = AvailablePortFinder.find();
+
+    @Test
+    void staticPathAnswersTheBasePathWhenTheTemplateStartedFirst() {
+        assertResponse(getPort(), "/orders", 200, "all orders");
+        assertResponse(getPort(), "/orders/", 200, "all orders");
+        assertResponse(getPort(), "/orders/7", 200, "order 7");
+    }
+
+    @Test
+    void stopTemplateRouteStartedBeforeTheStaticRoute() throws Exception {
+        context.getRouteController().stopRoute("order");
+
+        // the static path is served as before, and the template path is gone
+        assertResponse(getPort(), "/orders", 200, "all orders");
+        assertResponse(getPort(), "/orders/7", 404, null);
+
+        // and the template path is served again after a restart
+        context.getRouteController().startRoute("order");
+        assertResponse(getPort(), "/orders/7", 200, "order 7");
+        assertResponse(getPort(), "/orders", 200, "all orders");
+    }
+
+    @Test
+    void stopStaticRouteThenTemplateRoute() throws Exception {
+        context.getRouteController().stopRoute("orders");
+        assertResponse(getPort(), "/orders/7", 200, "order 7");
+
+        context.getRouteController().stopRoute("order");
+        assertResponse(getPort(), "/orders/7", 404, null);
+        assertResponse(getPort(), "/orders", 404, null);
+
+        context.getRouteController().startRoute("orders");
+        assertResponse(getPort(), "/orders", 200, "all orders");
+        assertResponse(getPort(), "/orders/7", 404, null);
+    }
+
+    @Test
+    void exactStaticPathDoesNotAnswerADeeperPathThatNoTemplateMatches() throws 
Exception {
+        // as without the template route: 404, whichever route started first
+        assertResponse(getPort(), "/orders/a/b", 404, null);
+        assertResponse(getPort2(), "/shop/a/b", 404, null);
+
+        // and again after the static route is stopped and started while the 
template route runs
+        context.getRouteController().stopRoute("orders");
+        context.getRouteController().startRoute("orders");
+        assertResponse(getPort(), "/orders/a/b", 404, null);
+        assertResponse(getPort(), "/orders", 200, "all orders");
+    }
+
+    @Test
+    void exactStaticPathWithMethodRestrictDoesNotAnswerADeeperPath() {
+        // the static route of /m only accepts POST: a deeper path gets 404, 
not the 405 of /m
+        assertResponse(getPort(), "/m/a/b", 404, null);
+    }
+
+    @Test
+    void prefixPathAnswersTheBasePathAndWhatNoTemplateMatches() {
+        assertResponse(getPort(), "/items", 200, "items");
+        assertResponse(getPort(), "/items/", 200, "items");
+        assertResponse(getPort(), "/items/a/b", 200, "items");
+        assertResponse(getPort(), "/items/7", 200, "item 7");
+    }
+
+    @Test
+    void staticRouteWithMethodRestrictAnswersTheBasePath() {
+        // the template route starts first; the static route of the base path 
only accepts POST
+        assertResponse(getPort(), "/m", 405, null);
+        Exchange out = template.request("undertow:http://localhost:"; + 
getPort() + "/m?throwExceptionOnFailure=false",
+                e -> {
+                    e.getMessage().setHeader(Exchange.HTTP_METHOD, "POST");
+                    e.getMessage().setBody("x");
+                });
+        assertEquals(200, 
out.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class));
+        assertEquals("post m", out.getMessage().getBody(String.class));
+        assertResponse(getPort(), "/m/7", 200, "m 7");
+    }
+
+    @Test
+    void rootPrefixPathAnswersTheRoot() {
+        // on the third port, a matchOnUriPrefix route of / and a template 
/{id}: Undertow serves the prefix path / as
+        // its default handler, which keeps the whole path as the rest
+        assertResponse(port3.getPort(), "/", 200, "root");
+        assertResponse(port3.getPort(), "/7", 200, "root id 7");
+        assertResponse(port3.getPort(), "/a/b", 200, "root");
+    }
+
+    @Test
+    void stopTemplateRouteStartedAfterThePrefixRoute() throws Exception {
+        context.getRouteController().stopRoute("item");
+
+        assertResponse(getPort(), "/items/a/b", 200, "items");
+        assertResponse(getPort(), "/items/7", 200, "items");
+        assertResponse(getPort(), "/items", 200, "items");
+
+        context.getRouteController().startRoute("item");
+        assertResponse(getPort(), "/items/7", 200, "item 7");
+        assertResponse(getPort(), "/items/a/b", 200, "items");
+    }
+
+    @Test
+    void stopTemplateRouteKeepsTheServerOfTheStaticRouteRunning() throws 
Exception {
+        // on the second port, the static path and the template are the only 
paths of the server
+        assertResponse(getPort2(), "/shop", 200, "shop");
+        assertResponse(getPort2(), "/shop/7", 200, "article 7");
+
+        context.getRouteController().stopRoute("article");
+
+        assertResponse(getPort2(), "/shop", 200, "shop");
+        assertResponse(getPort2(), "/shop/7", 404, null);
+
+        // the static route stops too, and starts again
+        context.getRouteController().stopRoute("shop");
+        context.getRouteController().startRoute("shop");
+        assertResponse(getPort2(), "/shop", 200, "shop");
+    }
+
+    private void assertResponse(int port, String path, int code, String body) {
+        Exchange out = template.request("undertow:http://localhost:"; + port + 
path + "?throwExceptionOnFailure=false", null);
+        assertEquals(code, 
out.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class), path);
+        if (body != null) {
+            assertEquals(body, out.getMessage().getBody(String.class), path);
+        }
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                // the template path starts first
+                
from("undertow:http://localhost:{{port}}/orders/{id}";).routeId("order")
+                        .setBody(simple("order ${header.id}"));
+                
from("undertow:http://localhost:{{port}}/orders";).routeId("orders")
+                        .setBody(constant("all orders"));
+
+                // the prefix path starts first
+                
from("undertow:http://localhost:{{port}}/items?matchOnUriPrefix=true";).routeId("items")
+                        .setBody(constant("items"));
+                
from("undertow:http://localhost:{{port}}/items/{id}";).routeId("item")
+                        .setBody(simple("item ${header.id}"));
+
+                // the template path starts first, then a static path that 
only accepts POST
+                
from("undertow:http://localhost:{{port}}/m/{id}";).routeId("mId")
+                        .setBody(simple("m ${header.id}"));
+                
from("undertow:http://localhost:{{port}}/m?httpMethodRestrict=POST";).routeId("m")
+                        .setBody(constant("post m"));
+
+                // the static path starts first, on a server of its own
+                
from("undertow:http://localhost:{{port2}}/shop";).routeId("shop")
+                        .setBody(constant("shop"));
+                
from("undertow:http://localhost:{{port2}}/shop/{id}";).routeId("article")
+                        .setBody(simple("article ${header.id}"));
+
+                // the root path
+                from("undertow:http://localhost:"; + port3.getPort() + 
"/?matchOnUriPrefix=true").routeId("root")
+                        .setBody(constant("root"));
+                from("undertow:http://localhost:"; + port3.getPort() + 
"/{id}").routeId("rootId")
+                        .setBody(simple("root id ${header.id}"));
+            }
+        };
+    }
+}
diff --git 
a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/handlers/CamelRootHandlerTest.java
 
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/handlers/CamelRootHandlerTest.java
index 9c4802a63565..49dad231fd1f 100644
--- 
a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/handlers/CamelRootHandlerTest.java
+++ 
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/handlers/CamelRootHandlerTest.java
@@ -20,6 +20,7 @@ import io.undertow.server.HttpHandler;
 import io.undertow.server.handlers.RedirectHandler;
 import org.junit.jupiter.api.Test;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -77,4 +78,77 @@ public class CamelRootHandlerTest {
 
     }
 
+    @Test
+    public void removeTemplateKeepsTheStaticPath() {
+        // whichever comes first: the static path keeps the server's root 
handler from being empty, and nothing is left
+        // once both are removed
+        for (boolean templateFirst : new boolean[] { true, false }) {
+            final CamelRootHandler root = new 
CamelRootHandler(DEFAULT_HANDLER);
+            if (templateFirst) {
+                root.add("/orders/{id}", null, false, new 
RedirectHandler("http://template";));
+                root.add("/orders", null, false, new 
RedirectHandler("http://static";));
+            } else {
+                root.add("/orders", null, false, new 
RedirectHandler("http://static";));
+                root.add("/orders/{id}", null, false, new 
RedirectHandler("http://template";));
+            }
+            root.remove("/orders/{id}", null, false);
+            assertFalse(root.isEmpty(), "templateFirst=" + templateFirst + " " 
+ root);
+            root.remove("/orders", null, false);
+            assertTrue(root.isEmpty(), "templateFirst=" + templateFirst + " " 
+ root);
+        }
+    }
+
+    @Test
+    public void removeStaticPathThenTemplate() {
+        for (boolean templateFirst : new boolean[] { true, false }) {
+            final CamelRootHandler root = new 
CamelRootHandler(DEFAULT_HANDLER);
+            if (templateFirst) {
+                root.add("/orders/{id}", null, false, new 
RedirectHandler("http://template";));
+                root.add("/orders", null, false, new 
RedirectHandler("http://static";));
+            } else {
+                root.add("/orders", null, false, new 
RedirectHandler("http://static";));
+                root.add("/orders/{id}", null, false, new 
RedirectHandler("http://template";));
+            }
+            root.remove("/orders", null, false);
+            assertFalse(root.isEmpty(), "templateFirst=" + templateFirst + " " 
+ root);
+            root.remove("/orders/{id}", null, false);
+            assertTrue(root.isEmpty(), "templateFirst=" + templateFirst + " " 
+ root);
+        }
+    }
+
+    @Test
+    public void removeTemplateKeepsThePrefixPath() {
+        final CamelRootHandler root = new CamelRootHandler(DEFAULT_HANDLER);
+        root.add("/items", null, true, new RedirectHandler("http://prefix";));
+        root.add("/items/{id}", null, false, new 
RedirectHandler("http://template";));
+        root.remove("/items/{id}", null, false);
+        assertFalse(root.isEmpty(), root.toString());
+        root.remove("/items", null, true);
+        assertTrue(root.isEmpty(), root.toString());
+    }
+
+    @Test
+    public void removeTemplateThatIsNotRegistered() {
+        final CamelRootHandler root = new CamelRootHandler(DEFAULT_HANDLER);
+        root.add("/orders", null, false, new RedirectHandler("http://static";));
+        // the base path has only a static path: nothing to remove
+        root.remove("/orders/{id}", null, false);
+        assertFalse(root.isEmpty(), root.toString());
+        root.remove("/orders", null, false);
+        assertTrue(root.isEmpty(), root.toString());
+    }
+
+    @Test
+    public void removeExactAndPrefixPathsThatShareAHandler() {
+        // an exact and a prefix route of one path share the handler of the 
first one: whichever is removed last removes
+        // its registration
+        final CamelRootHandler root = new CamelRootHandler(DEFAULT_HANDLER);
+        root.add("/p", "GET", false, new RedirectHandler("http://exact";));
+        root.add("/p", "POST", true, new RedirectHandler("http://prefix";));
+        root.remove("/p", "GET", false);
+        assertFalse(root.isEmpty(), root.toString());
+        root.remove("/p", "POST", true);
+        assertTrue(root.isEmpty(), root.toString());
+        assertEquals("CamelPathHandler[exact={}, prefix={}]", root.toString());
+    }
 }
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index bfbe13f9cc4f..5d88bf55901e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -3427,6 +3427,20 @@ gets it, even when the plain path is more specific (for 
example a plain `/users/
 path with a template (such as `/api/x/y` for `+/api/{name}+`) that no template 
matches now gets 404 (it got 405
 before).
 
+=== camel-undertow - the static route of a base path answers it next to a path 
template
+
+When one route consumes a path template, such as `+/orders/{id}+`, and another 
route the static path of its base
+path (`/orders`, exact or with `matchOnUriPrefix=true`), a request for 
`/orders` itself (also `/orders/`, and `/`
+for templates of the root path) now goes to the static route. Before, the 
template route answered it with an empty
+`id` when it had started first, or when the static route used 
`matchOnUriPrefix`. When the static route restricts
+the HTTP methods (`httpMethodRestrict`), such a request with another method 
now gets 405 instead of reaching the
+template route. A `matchOnUriPrefix` route of the base path now also starts 
when the template route started first
+(it failed with `Duplicate handlers on a path`). An exact static route 
(without `matchOnUriPrefix`) now answers
+only its own path: a deeper path that no template matches, such as 
`/orders/a/b`, gets 404, as without the template
+route (before, the static route answered it, or it got 405 when the static 
route restricted the methods). To keep
+the old behaviour, where the template route answers `/orders` with an empty 
`id`, drop the static route of the base
+path: a template route alone still serves the base path.
+
 === camel-huaweicloud-obs - a listed object that cannot be got is skipped
 
 When the consumer lists the bucket, a failure to get one of the listed objects 
(for example access denied, or an

Reply via email to