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 607efa1d7174 CAMEL-26051: camel-undertow, camel-vertx-http - put the 
CamelHttpPath header before the query of the endpoint uri (#28538)
607efa1d7174 is described below

commit 607efa1d7174f2bd49f99246872b00383636bc38
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Oct 27 15:42:11 2026 +0530

    CAMEL-26051: camel-undertow, camel-vertx-http - put the CamelHttpPath 
header before the query of the endpoint uri (#28538)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/component/undertow/UndertowHelper.java   |  10 +-
 ...towProducerPathHeaderWithEndpointQueryTest.java | 143 ++++++++++++++++++++
 .../component/vertx/http/VertxHttpHelper.java      |  10 +-
 .../VertxHttpPathHeaderWithEndpointQueryTest.java  | 150 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  15 +++
 5 files changed, 322 insertions(+), 6 deletions(-)

diff --git 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowHelper.java
 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowHelper.java
index 318e407a99a3..198ec9253f45 100644
--- 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowHelper.java
+++ 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowHelper.java
@@ -64,12 +64,16 @@ public final class UndertowHelper {
                 path = path.substring(1);
             }
             if (!path.isEmpty()) {
+                // insert the path before the query of the uri, if it has one 
(as camel-http does)
+                int idx = uri.indexOf('?');
+                String base = idx == -1 ? uri : uri.substring(0, idx);
+                String query = idx == -1 ? "" : uri.substring(idx);
                 // make sure that there is exactly one "/" between HTTP_URI and
                 // HTTP_PATH
-                if (!uri.endsWith("/")) {
-                    uri = uri + "/";
+                if (!base.endsWith("/")) {
+                    base = base + "/";
                 }
-                uri = uri.concat(path);
+                uri = base + path + query;
             }
         }
 
diff --git 
a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowProducerPathHeaderWithEndpointQueryTest.java
 
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowProducerPathHeaderWithEndpointQueryTest.java
new file mode 100644
index 000000000000..0fe6f1ad7dac
--- /dev/null
+++ 
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowProducerPathHeaderWithEndpointQueryTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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 java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * The CamelHttpPath header is appended to the path of the endpoint URI, also 
when the endpoint URI has a query: the
+ * path goes before the query, as with camel-http.
+ */
+class UndertowProducerPathHeaderWithEndpointQueryTest extends CamelTestSupport 
{
+
+    @RegisterExtension
+    AvailablePortFinder.Port proxyPort = AvailablePortFinder.find();
+
+    private final List<String> requests = new CopyOnWriteArrayList<>();
+    private HttpServer server;
+
+    @BeforeEach
+    void startServer() throws Exception {
+        server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
+        server.createContext("/", ex -> {
+            requests.add(
+                    ex.getRequestMethod() + " " + 
ex.getRequestURI().getRawPath() + " ? " + ex.getRequestURI().getRawQuery());
+            ex.getRequestBody().readAllBytes();
+            byte[] ok = "ok".getBytes(StandardCharsets.UTF_8);
+            ex.sendResponseHeaders(200, ok.length);
+            ex.getResponseBody().write(ok);
+            ex.close();
+        });
+        server.start();
+    }
+
+    @AfterEach
+    void stopServer() {
+        server.stop(0);
+    }
+
+    private String endpoint(String pathAndQuery) {
+        return "undertow:http://localhost:"; + server.getAddress().getPort() + 
pathAndQuery;
+    }
+
+    @Test
+    void pathHeaderWithEndpointQuery() {
+        template.sendBodyAndHeader(endpoint("/orders?apiVersion=2"), null, 
Exchange.HTTP_PATH, "/123");
+
+        assertEquals(List.of("GET /orders/123 ? apiVersion=2"), requests);
+    }
+
+    @Test
+    void pathHeaderWithEndpointQueryAndQueryHeader() {
+        template.sendBodyAndHeaders(endpoint("/orders?apiVersion=2"), null,
+                Map.of(Exchange.HTTP_PATH, "/123", Exchange.HTTP_QUERY, 
"expand=items"));
+
+        assertEquals(List.of("GET /orders/123 ? expand=items"), requests);
+    }
+
+    @Test
+    void pathHeaderWithEndpointQueryPost() {
+        template.sendBodyAndHeaders(endpoint("/orders?apiVersion=2"), 
"{\"qty\":1}",
+                Map.of(Exchange.HTTP_PATH, "/123/lines", Exchange.HTTP_METHOD, 
"POST"));
+
+        assertEquals(List.of("POST /orders/123/lines ? apiVersion=2"), 
requests);
+    }
+
+    @Test
+    void pathHeaderWithEndpointQueryAndTrailingSlash() {
+        template.sendBodyAndHeader(endpoint("/orders/?apiVersion=2"), null, 
Exchange.HTTP_PATH, "123");
+
+        assertEquals(List.of("GET /orders/123 ? apiVersion=2"), requests);
+    }
+
+    @Test
+    void pathHeaderWithEndpointQueryAndNoEndpointPath() {
+        template.sendBodyAndHeader(endpoint("?apiVersion=2"), null, 
Exchange.HTTP_PATH, "/123");
+
+        assertEquals(List.of("GET /123 ? apiVersion=2"), requests);
+    }
+
+    @Test
+    void proxyRouteWithEndpointQuerySendsTheRemainingPath() throws Exception {
+        // the undertow consumer sets CamelHttpPath to the rest of the request 
path, so the backend gets it after the
+        // endpoint path; removing the header keeps the endpoint path alone. 
The consumer also sets CamelHttpQuery from
+        // the request, which wins over the query of the endpoint, as before
+        String proxy = "undertow:http://localhost:"; + proxyPort.getPort();
+        String backend = endpoint("/svc?key=1");
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from(proxy + "/api?matchOnUriPrefix=true")
+                        .to(backend);
+                from(proxy + "/old?matchOnUriPrefix=true")
+                        .removeHeader(Exchange.HTTP_PATH)
+                        .to(backend);
+            }
+        });
+
+        template.requestBody(proxy + "/api/orders/123?expand=items", null, 
String.class);
+        template.requestBody(proxy + "/old/orders/123?expand=items", null, 
String.class);
+
+        assertEquals(List.of("GET /svc/orders/123 ? expand=items", "GET /svc ? 
expand=items"), requests);
+    }
+
+    @Test
+    void pathHeaderWithoutEndpointQuery() {
+        // control: unchanged without a query in the endpoint uri
+        template.sendBodyAndHeader(endpoint("/orders"), null, 
Exchange.HTTP_PATH, "/123");
+        template.sendBodyAndHeaders(endpoint("/orders"), null,
+                Map.of(Exchange.HTTP_PATH, "/123", Exchange.HTTP_QUERY, 
"expand=items"));
+
+        assertEquals(List.of("GET /orders/123 ? null", "GET /orders/123 ? 
expand=items"), requests);
+    }
+}
diff --git 
a/components/camel-vertx/camel-vertx-http/src/main/java/org/apache/camel/component/vertx/http/VertxHttpHelper.java
 
b/components/camel-vertx/camel-vertx-http/src/main/java/org/apache/camel/component/vertx/http/VertxHttpHelper.java
index 26978592aff0..4b87d88096a1 100644
--- 
a/components/camel-vertx/camel-vertx-http/src/main/java/org/apache/camel/component/vertx/http/VertxHttpHelper.java
+++ 
b/components/camel-vertx/camel-vertx-http/src/main/java/org/apache/camel/component/vertx/http/VertxHttpHelper.java
@@ -64,12 +64,16 @@ public final class VertxHttpHelper {
                 path = path.substring(1);
             }
             if (!path.isEmpty()) {
+                // insert the path before the query of the uri, if it has one 
(as camel-http does)
+                int idx = uri.indexOf('?');
+                String base = idx == -1 ? uri : uri.substring(0, idx);
+                String query = idx == -1 ? "" : uri.substring(idx);
                 // make sure that there is exactly one "/" between HTTP_URI and
                 // HTTP_PATH
-                if (!uri.endsWith("/")) {
-                    uri = uri + "/";
+                if (!base.endsWith("/")) {
+                    base = base + "/";
                 }
-                uri = uri.concat(path);
+                uri = base + path + query;
             }
         }
 
diff --git 
a/components/camel-vertx/camel-vertx-http/src/test/java/org/apache/camel/component/vertx/http/VertxHttpPathHeaderWithEndpointQueryTest.java
 
b/components/camel-vertx/camel-vertx-http/src/test/java/org/apache/camel/component/vertx/http/VertxHttpPathHeaderWithEndpointQueryTest.java
new file mode 100644
index 000000000000..a7b1faeef6b0
--- /dev/null
+++ 
b/components/camel-vertx/camel-vertx-http/src/test/java/org/apache/camel/component/vertx/http/VertxHttpPathHeaderWithEndpointQueryTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.vertx.http;
+
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * The CamelHttpPath header is appended to the path of the endpoint URI, also 
when the endpoint URI has a query: the
+ * path goes before the query, as with camel-http.
+ */
+class VertxHttpPathHeaderWithEndpointQueryTest extends CamelTestSupport {
+
+    @RegisterExtension
+    AvailablePortFinder.Port proxyPort = AvailablePortFinder.find();
+
+    private final List<String> requests = new CopyOnWriteArrayList<>();
+    private HttpServer server;
+
+    @BeforeEach
+    void startServer() throws Exception {
+        server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
+        server.createContext("/", ex -> {
+            requests.add(
+                    ex.getRequestMethod() + " " + 
ex.getRequestURI().getRawPath() + " ? " + ex.getRequestURI().getRawQuery());
+            ex.getRequestBody().readAllBytes();
+            byte[] ok = "ok".getBytes(StandardCharsets.UTF_8);
+            ex.sendResponseHeaders(200, ok.length);
+            ex.getResponseBody().write(ok);
+            ex.close();
+        });
+        server.start();
+    }
+
+    @AfterEach
+    void stopServer() {
+        server.stop(0);
+    }
+
+    private String endpoint(String pathAndQuery) {
+        return "vertx-http:http://localhost:"; + server.getAddress().getPort() 
+ pathAndQuery;
+    }
+
+    @Test
+    void pathHeaderWithEndpointQuery() {
+        template.sendBodyAndHeader(endpoint("/orders?apiVersion=2"), null, 
Exchange.HTTP_PATH, "/123");
+
+        assertEquals(List.of("GET /orders/123 ? apiVersion=2"), requests);
+    }
+
+    @Test
+    void pathHeaderWithEndpointQueryAndQueryHeader() {
+        template.sendBodyAndHeaders(endpoint("/orders?apiVersion=2"), null,
+                Map.of(Exchange.HTTP_PATH, "/123", Exchange.HTTP_QUERY, 
"expand=items"));
+
+        assertEquals(List.of("GET /orders/123 ? expand=items"), requests);
+    }
+
+    @Test
+    void pathHeaderWithEndpointQueryPost() {
+        template.sendBodyAndHeaders(endpoint("/orders?apiVersion=2"), 
"{\"qty\":1}",
+                Map.of(Exchange.HTTP_PATH, "/123/lines", Exchange.HTTP_METHOD, 
"POST"));
+
+        assertEquals(List.of("POST /orders/123/lines ? apiVersion=2"), 
requests);
+    }
+
+    @Test
+    void pathHeaderWithEndpointQueryAndTrailingSlash() {
+        template.sendBodyAndHeader(endpoint("/orders/?apiVersion=2"), null, 
Exchange.HTTP_PATH, "123");
+
+        assertEquals(List.of("GET /orders/123 ? apiVersion=2"), requests);
+    }
+
+    @Test
+    void pathHeaderWithEndpointQueryAndNoEndpointPath() {
+        template.sendBodyAndHeader(endpoint("?apiVersion=2"), null, 
Exchange.HTTP_PATH, "/123");
+
+        assertEquals(List.of("GET /123 ? apiVersion=2"), requests);
+    }
+
+    @Test
+    void encodedPathHeaderWithEndpointQuery() {
+        template.sendBodyAndHeader(endpoint("/orders?apiVersion=2"), null, 
Exchange.HTTP_PATH, "/a%20b");
+
+        assertEquals(List.of("GET /orders/a%20b ? apiVersion=2"), requests);
+    }
+
+    @Test
+    void proxyRouteWithEndpointQuerySendsTheRemainingPath() throws Exception {
+        // the undertow consumer sets CamelHttpPath to the rest of the request 
path, so the backend gets it after the
+        // endpoint path; removing the header keeps the endpoint path alone. 
The consumer also sets CamelHttpQuery from
+        // the request, which wins over the query of the endpoint, as before
+        String proxy = "undertow:http://localhost:"; + proxyPort.getPort();
+        String backend = endpoint("/svc?key=1&bridgeEndpoint=true");
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from(proxy + "/api?matchOnUriPrefix=true")
+                        .to(backend);
+                from(proxy + "/old?matchOnUriPrefix=true")
+                        .removeHeader(Exchange.HTTP_PATH)
+                        .to(backend);
+            }
+        });
+
+        template.requestBody(proxy + "/api/orders/123?expand=items", null, 
String.class);
+        template.requestBody(proxy + "/old/orders/123?expand=items", null, 
String.class);
+
+        assertEquals(List.of("GET /svc/orders/123 ? expand=items", "GET /svc ? 
expand=items"), requests);
+    }
+
+    @Test
+    void pathHeaderWithoutEndpointQuery() {
+        // control: unchanged without a query in the endpoint uri
+        template.sendBodyAndHeader(endpoint("/orders"), null, 
Exchange.HTTP_PATH, "/123");
+        template.sendBodyAndHeaders(endpoint("/orders"), null,
+                Map.of(Exchange.HTTP_PATH, "/123", Exchange.HTTP_QUERY, 
"expand=items"));
+
+        assertEquals(List.of("GET /orders/123 ? null", "GET /orders/123 ? 
expand=items"), requests);
+    }
+}
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 533ace73aa67..e97822ca9b00 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
@@ -1798,6 +1798,21 @@ first ran is no longer served: its messages no longer 
reach the route and it no
 sends, and the connection is closed. Clients that open a connection in this 
window must reconnect after the consumer
 has started.
 
+=== camel-undertow, camel-vertx-http - CamelHttpPath with a query in the 
endpoint uri
+
+When the endpoint uri of an undertow or vertx-http producer has a query, the 
`CamelHttpPath` header now goes before
+that query, as in camel-http. Before, camel-undertow dropped the header path, 
and camel-vertx-http put it into the
+query (or dropped it when `CamelHttpQuery` was set). The query is chosen as 
before: a `CamelHttpQuery` header wins over
+the query of the endpoint.
+
+This changes the target of routes that bridge an HTTP consumer to such a 
producer, as the HTTP consumers (undertow,
+platform-http-vertx, servlet) set `CamelHttpPath` from the path of the 
request. For example the route
+`from("undertow:http://0.0.0.0:8080/api?matchOnUriPrefix=true";).to("undertow:http://backend/svc?key=1";)`,
 or the same
+with a `vertx-http:` producer and `bridgeEndpoint=true`, now sends a request 
for `/api/orders/123` to
+`/svc/orders/123` instead of `/svc`. To keep the old target, remove the header 
before the producer, for example with
+`removeHeader(Exchange.HTTP_PATH)`. The path comes from the client request, 
and the producer sends it with the query
+of the endpoint (`key=1` in the example): a proxy route should validate or 
restrict the path, or remove the header.
+
 === camel-dapr - the saveBulk and executeTransaction state operations are sent
 
 The `saveBulk` and `executeTransaction` operations of `dapr:state` built their 
request but never sent it to Dapr, so

Reply via email to