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