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