This is an automated email from the ASF dual-hosted git repository.
Croway 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 bda1be58a6cd CAMEL-24625: camel-platform-http: add stripUriPrefix
option for path-based reverse proxies (#26114)
bda1be58a6cd is described below
commit bda1be58a6cd61025ba3a942c7836ab0688466ce
Author: Federico Mariani <[email protected]>
AuthorDate: Mon Sep 7 18:52:13 2026 +0200
CAMEL-24625: camel-platform-http: add stripUriPrefix option for path-based
reverse proxies (#26114)
* CAMEL-24625: camel-platform-http - add stripUriPrefix option for
path-based reverse proxies
platform-http is the only HTTP consumer (unlike camel-servlet, camel-jetty,
camel-netty-http and camel-undertow) that does not make CamelHttpPath
relative to the consumer's own registered path, so combining it with the
http producer's bridgeEndpoint option to build a path-based reverse proxy
forwards the full raw request path instead of the path relative to the
consumer.
Add a new consumer option, stripUriPrefix (default false), and a shared
HttpHelper.stripUriPrefix(requestPath, consumerPath) helper in
camel-http-base that other HTTP consumers could reuse. The helper is a
defensive pure function: it only strips a full, boundary-respecting match
of the consumer path (REST-DSL {name} placeholders included) and otherwise
returns the request path unchanged - in particular a consumer path of "/"
(the platform-http:proxy pseudo-path) is always a no-op, so the existing
platform-http:proxy forward-proxy mode is unaffected.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tgb2b9DSh1ncG7C82rwzsE
* CAMEL-24625: camel-platform-http-vertx - wire up stripUriPrefix
Apply HttpHelper.stripUriPrefix to CamelHttpPath in
VertxPlatformHttpConsumer.populateCamelMessage when the new consumer
option is enabled, using the endpoint's own registered path as the
consumer path. This is the single place headers are (re)populated for
a request that reaches the route - the OAuth security handler path
resets the message and lets processHttpRequest/populateCamelMessage
repopulate headers afterwards, so no separate handling is needed there.
CamelHttpUri/CamelHttpUrl are untouched, only CamelHttpPath is rewritten.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tgb2b9DSh1ncG7C82rwzsE
* CAMEL-24625: camel-platform-http - document stripUriPrefix path-based
reverse proxy
Add a short section next to the existing reverse-proxy documentation
showing the stripUriPrefix + bridgeEndpoint combination, with a
before/after CamelHttpPath example and a cross-reference clarifying
this is distinct from the platform-http:proxy Host-header forward
proxy.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tgb2b9DSh1ncG7C82rwzsE
* Regen
* CAMEL-24625: stripUriPrefix - case-sensitive segment match and
exact-match docs/test
Address review feedback: match literal path segments case-sensitively (the
Vert.x router only ever routes case-exact prefixes, so this aligns the
helper
with reachable behaviour and HTTP path semantics), drop the two unit cases
that asserted unreachable case-insensitive matching, verify exact-match
stripping in the live Vert.x test without matchOnUriPrefix, and document
that
matchOnUriPrefix is not a prerequisite for stripUriPrefix.
Co-authored-by: Claude Code <[email protected]>
---------
Co-authored-by: Claude Sonnet 5 <[email protected]>
Co-authored-by: github-actions[bot]
<github-actions[bot]@users.noreply.github.com>
---
.../camel/catalog/components/platform-http.json | 21 ++--
.../catalog/docs/platform-http-component.adoc | 31 +++++
.../org/apache/camel/http/base/HttpHelper.java | 86 +++++++++++++
.../http/base/HttpHelperStripUriPrefixTest.java | 70 +++++++++++
components/camel-platform-http-vertx/pom.xml | 4 +
.../http/vertx/VertxPlatformHttpConsumer.java | 5 +
.../vertx/VertxPlatformHttpStripUriPrefixTest.java | 140 +++++++++++++++++++++
.../http/PlatformHttpEndpointConfigurer.java | 6 +
.../http/PlatformHttpEndpointUriFactory.java | 3 +-
.../component/platform/http/platform-http.json | 21 ++--
.../src/main/docs/platform-http-component.adoc | 31 +++++
.../platform/http/PlatformHttpEndpoint.java | 27 ++++
.../PlatformHttpEndpointStripUriPrefixTest.java | 69 ++++++++++
.../dsl/PlatformHttpEndpointBuilderFactory.java | 54 ++++++++
14 files changed, 547 insertions(+), 21 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/platform-http.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/platform-http.json
index e2cdf3cf26fb..d9dc24f3a08c 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/platform-http.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/platform-http.json
@@ -50,15 +50,16 @@
"produces": { "index": 13, "kind": "parameter", "displayName": "Produces",
"group": "consumer", "label": "consumer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "description": "The content type this endpoint produces, such
as application\/xml or application\/json." },
"requestTimeout": { "index": 14, "kind": "parameter", "displayName":
"Request Timeout", "group": "consumer", "label": "advanced,consumer",
"required": false, "type": "integer", "javaType": "long", "deprecated": false,
"autowired": false, "secret": false, "description": "The period in milliseconds
after which the request should be timed out." },
"returnHttpRequestHeaders": { "index": 15, "kind": "parameter",
"displayName": "Return Http Request Headers", "group": "consumer", "label":
"advanced,consumer", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether to include HTTP request headers
(Accept, User-Agent, etc.) into HTTP response produced by this endpoint." },
- "useBodyHandler": { "index": 16, "kind": "parameter", "displayName": "Use
Body Handler", "group": "consumer", "label": "advanced,consumer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether to use BodyHandler for the request. If set to false then the request
will no be read and parsed." },
- "useCookieHandler": { "index": 17, "kind": "parameter", "displayName":
"Use Cookie Handler", "group": "consumer", "label": "advanced,consumer",
"required": false, "type": "boolean", "javaType": "boolean", "deprecated":
false, "autowired": false, "secret": false, "defaultValue": false,
"description": "Whether to enable the Cookie Handler that allows Cookie
addition, expiry, and retrieval (currently only supported by
camel-platform-http-vertx)" },
- "useStreaming": { "index": 18, "kind": "parameter", "displayName": "Use
Streaming", "group": "consumer", "label": "advanced,consumer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Whether to use streaming for large requests and responses (currently only
supported by camel-platform-http-vertx)" },
- "bridgeErrorHandler": { "index": 19, "kind": "parameter", "displayName":
"Bridge Error Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Allows for bridging the consumer to the
Camel routing Error Handler, which mean any exceptions (if possible) occurred
while the Camel consumer is trying to pickup incoming [...]
- "exceptionHandler": { "index": 20, "kind": "parameter", "displayName":
"Exception Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "object", "javaType":
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.",
"deprecated": false, "autowired": false, "secret": false, "description": "To
let the consumer use a custom ExceptionHandler. Notice if the option
bridgeErrorHandler is enabled then this option is not in use. By de [...]
- "exchangePattern": { "index": 21, "kind": "parameter", "displayName":
"Exchange Pattern", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "enum", "javaType":
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ],
"deprecated": false, "autowired": false, "secret": false, "description": "Sets
the exchange pattern when the consumer creates an exchange." },
- "fileNameExtWhitelist": { "index": 22, "kind": "parameter", "displayName":
"File Name Ext Whitelist", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"description": "A comma or whitespace separated list of file extensions.
Uploads having these extensions will be stored locally. Null value or asterisk
() will allow all files." },
- "headerFilterStrategy": { "index": 23, "kind": "parameter", "displayName":
"Header Filter Strategy", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType":
"org.apache.camel.spi.HeaderFilterStrategy", "deprecated": false, "autowired":
false, "secret": false, "description": "To use a custom HeaderFilterStrategy to
filter headers to and from Camel message." },
- "platformHttpEngine": { "index": 24, "kind": "parameter", "displayName":
"Platform Http Engine", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType":
"org.apache.camel.component.platform.http.spi.PlatformHttpEngine",
"deprecated": false, "autowired": false, "secret": false, "description": "An
HTTP Server engine implementation to serve the requests of this endpoint." },
- "oauthProfile": { "index": 25, "kind": "parameter", "displayName": "OAuth
Profile", "group": "security", "label": "consumer,security", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "OAuth profile name for
validating incoming Authorization: Bearer tokens. When set, the request is
authenticated before the route is processed. This requires an
OAuthTokenValidationFactory; camel-oauth provides [...]
+ "stripUriPrefix": { "index": 16, "kind": "parameter", "displayName":
"Strip Uri Prefix", "group": "consumer", "label": "consumer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Whether to strip the registered consumer path from CamelHttpPath after the
request has been matched, so the exchange sees the path relative to this
consumer instead of the full raw request path. Comb [...]
+ "useBodyHandler": { "index": 17, "kind": "parameter", "displayName": "Use
Body Handler", "group": "consumer", "label": "advanced,consumer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether to use BodyHandler for the request. If set to false then the request
will no be read and parsed." },
+ "useCookieHandler": { "index": 18, "kind": "parameter", "displayName":
"Use Cookie Handler", "group": "consumer", "label": "advanced,consumer",
"required": false, "type": "boolean", "javaType": "boolean", "deprecated":
false, "autowired": false, "secret": false, "defaultValue": false,
"description": "Whether to enable the Cookie Handler that allows Cookie
addition, expiry, and retrieval (currently only supported by
camel-platform-http-vertx)" },
+ "useStreaming": { "index": 19, "kind": "parameter", "displayName": "Use
Streaming", "group": "consumer", "label": "advanced,consumer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Whether to use streaming for large requests and responses (currently only
supported by camel-platform-http-vertx)" },
+ "bridgeErrorHandler": { "index": 20, "kind": "parameter", "displayName":
"Bridge Error Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Allows for bridging the consumer to the
Camel routing Error Handler, which mean any exceptions (if possible) occurred
while the Camel consumer is trying to pickup incoming [...]
+ "exceptionHandler": { "index": 21, "kind": "parameter", "displayName":
"Exception Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "object", "javaType":
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.",
"deprecated": false, "autowired": false, "secret": false, "description": "To
let the consumer use a custom ExceptionHandler. Notice if the option
bridgeErrorHandler is enabled then this option is not in use. By de [...]
+ "exchangePattern": { "index": 22, "kind": "parameter", "displayName":
"Exchange Pattern", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "enum", "javaType":
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ],
"deprecated": false, "autowired": false, "secret": false, "description": "Sets
the exchange pattern when the consumer creates an exchange." },
+ "fileNameExtWhitelist": { "index": 23, "kind": "parameter", "displayName":
"File Name Ext Whitelist", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"description": "A comma or whitespace separated list of file extensions.
Uploads having these extensions will be stored locally. Null value or asterisk
() will allow all files." },
+ "headerFilterStrategy": { "index": 24, "kind": "parameter", "displayName":
"Header Filter Strategy", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType":
"org.apache.camel.spi.HeaderFilterStrategy", "deprecated": false, "autowired":
false, "secret": false, "description": "To use a custom HeaderFilterStrategy to
filter headers to and from Camel message." },
+ "platformHttpEngine": { "index": 25, "kind": "parameter", "displayName":
"Platform Http Engine", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType":
"org.apache.camel.component.platform.http.spi.PlatformHttpEngine",
"deprecated": false, "autowired": false, "secret": false, "description": "An
HTTP Server engine implementation to serve the requests of this endpoint." },
+ "oauthProfile": { "index": 26, "kind": "parameter", "displayName": "OAuth
Profile", "group": "security", "label": "consumer,security", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "OAuth profile name for
validating incoming Authorization: Bearer tokens. When set, the request is
authenticated before the route is processed. This requires an
OAuthTokenValidationFactory; camel-oauth provides [...]
}
}
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/platform-http-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/platform-http-component.adoc
index 1b5790d7d0f1..0b551804037e 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/platform-http-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/platform-http-component.adoc
@@ -201,6 +201,37 @@ Platform HTTP component can act as a reverse proxy. In
that case, some headers a
At this moment, this feature is only supported for Quarkus implemented in
`camel-platform-http-vertx` component.
+=== Forwarding requests under a path prefix (path-based reverse proxy)
+
+This is a different scenario from the `platform-http:proxy` forward proxy
described above, which is a `Host` header
+based forward proxy and only supported in `camel-platform-http-vertx`. Here,
the consumer is registered under an
+ordinary path, e.g. `/reverse-proxy`, and requests received under that path
should be forwarded to a fixed backend
+with the prefix stripped, using the xref:components::http-component.adoc[http]
producer's `bridgeEndpoint` option.
+
+Enable the consumer option `stripUriPrefix` so the matched consumer path is
removed from `CamelHttpPath` before the
+route runs:
+
+[source,java]
+----
+from("platform-http:/reverse-proxy?matchOnUriPrefix=true&stripUriPrefix=true")
+ .to("http://backend?bridgeEndpoint=true");
+----
+
+`matchOnUriPrefix` is not required for `stripUriPrefix`; it is only needed to
also forward requests under sub-paths.
+On an exact match (e.g. `GET /reverse-proxy`) the path is stripped to `/`.
+
+|===
+| Incoming request | `CamelHttpPath` (`stripUriPrefix=false`, default) |
`CamelHttpPath` (`stripUriPrefix=true`) | Forwarded to
+
+| `GET /reverse-proxy/get`
+| `/reverse-proxy/get`
+| `/get`
+| `http://backend/get`
+|===
+
+`CamelHttpUri`/`CamelHttpUrl` still hold the full original request, only
`CamelHttpPath` is rewritten. This option is
+currently implemented in `camel-platform-http-vertx`.
+
=== File Attachments handling
Since Apache Camel 4.10, multipart file uploads are easier and harmonized
across all runtimes. When a single file is uploaded, the Apache Camel framework
provides the following:
diff --git
a/components/camel-http-base/src/main/java/org/apache/camel/http/base/HttpHelper.java
b/components/camel-http-base/src/main/java/org/apache/camel/http/base/HttpHelper.java
index 1a9198796eb1..b12cb19ee633 100644
---
a/components/camel-http-base/src/main/java/org/apache/camel/http/base/HttpHelper.java
+++
b/components/camel-http-base/src/main/java/org/apache/camel/http/base/HttpHelper.java
@@ -135,6 +135,92 @@ public final class HttpHelper {
return HttpUtil.isStatusCodeOk(statusCode, okStatusCodeRange);
}
+ /**
+ * Removes the leading portion of a request path that was matched by a
consumer's registered path, so that what
+ * remains is the path relative to that consumer - the same way {@code
camel-servlet}, {@code camel-jetty},
+ * {@code camel-netty-http} and {@code camel-undertow} already behave by
default.
+ * <p/>
+ * This is a pure, defensive function: it never throws for malformed
input, and whenever the match is anything less
+ * than a full, boundary-respecting match of every consumer path segment,
it returns {@code requestPath} unchanged
+ * rather than risk producing a partial or incorrect result. In particular
a {@code consumerPath} of {@code null},
+ * blank, {@code "/"} or {@code ""} (once normalized) is treated as "no
prefix to strip" and always returns
+ * {@code requestPath} unchanged - this is what makes the platform-http
{@code proxy} pseudo-path (whose consumer
+ * path is {@code "/"}) provably unaffected by callers of this method.
+ * <p/>
+ * The consumer path may contain REST-DSL style {@code {name}} placeholder
segments, which match any single
+ * non-empty request segment.
+ *
+ * @param requestPath the incoming request path, e.g. {@code
/reverse-proxy/get}
+ * @param consumerPath the path the consumer is registered under, e.g.
{@code /reverse-proxy} or
+ * {@code /reverse-proxy*}
+ * @return the remaining path after stripping the matched
consumer path, always starting with a
+ * {@code /}, or {@code requestPath} unchanged if the
consumer path does not match (or there is
+ * nothing to strip)
+ */
+ public static String stripUriPrefix(String requestPath, String
consumerPath) {
+ if (requestPath == null) {
+ return null;
+ }
+ if (consumerPath == null || consumerPath.isBlank()) {
+ return requestPath;
+ }
+
+ String normalized = consumerPath.trim();
+ if (!normalized.startsWith("/")) {
+ normalized = "/" + normalized;
+ }
+ if (normalized.endsWith("*")) {
+ normalized = normalized.substring(0, normalized.length() - 1);
+ }
+ while (normalized.length() > 1 && normalized.endsWith("/")) {
+ normalized = normalized.substring(0, normalized.length() - 1);
+ }
+ if (normalized.isEmpty() || "/".equals(normalized)) {
+ return requestPath;
+ }
+
+ // split using single char / is optimized in the jdk
+ final String[] consumerSegments = normalized.split("/");
+ final String[] requestSegments = requestPath.split("/", -1);
+
+ if (requestSegments.length < consumerSegments.length) {
+ return requestPath;
+ }
+
+ for (int i = 0; i < consumerSegments.length; i++) {
+ String consumerSegment = consumerSegments[i];
+ if (consumerSegment.isEmpty()) {
+ // leading empty segment produced by the split on the initial
'/'
+ continue;
+ }
+ String requestSegment = i < requestSegments.length ?
requestSegments[i] : null;
+ boolean placeholder = consumerSegment.startsWith("{") &&
consumerSegment.endsWith("}");
+ if (placeholder) {
+ if (requestSegment == null || requestSegment.isEmpty()) {
+ return requestPath;
+ }
+ } else if (requestSegment == null ||
!consumerSegment.equals(requestSegment)) {
+ return requestPath;
+ }
+ }
+
+ int matchedSegments = consumerSegments.length;
+ if (matchedSegments == requestSegments.length) {
+ // exact match, nothing remains
+ return "/";
+ }
+
+ // boundary check: the next request segment marks a '/' boundary, so
this is always safe - anything else
+ // (e.g. /reverse-proxyfoo when the consumer path is /reverse-proxy)
was already rejected above because the
+ // last matched request segment would not have equaled the consumer's
last segment
+ StringBuilder remainder = new StringBuilder();
+ for (int i = matchedSegments; i < requestSegments.length; i++) {
+ remainder.append('/').append(requestSegments[i]);
+ }
+ String result = remainder.toString();
+ return result.isEmpty() ? "/" : result;
+ }
+
/**
* In the endpoint the user may have defined rest {} placeholders. This
helper method map those placeholders with
* data from the incoming request context path
diff --git
a/components/camel-http-base/src/test/java/org/apache/camel/http/base/HttpHelperStripUriPrefixTest.java
b/components/camel-http-base/src/test/java/org/apache/camel/http/base/HttpHelperStripUriPrefixTest.java
new file mode 100644
index 000000000000..09dfae56a0d0
--- /dev/null
+++
b/components/camel-http-base/src/test/java/org/apache/camel/http/base/HttpHelperStripUriPrefixTest.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.http.base;
+
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class HttpHelperStripUriPrefixTest {
+
+ static Stream<Arguments> stripCases() {
+ return Stream.of(
+ Arguments.of("/reverse-proxy/get", "/reverse-proxy", "/get"),
+ Arguments.of("/reverse-proxy", "/reverse-proxy", "/"),
+ Arguments.of("/reverse-proxy/", "/reverse-proxy", "/"),
+ Arguments.of("/reverse-proxy/a/b/", "/reverse-proxy", "/a/b/"),
+ // leading slash on the consumer path is optional
+ Arguments.of("/reverse-proxy/get", "reverse-proxy", "/get"),
+ // Vert.x style trailing wildcard on the consumer path
+ Arguments.of("/reverse-proxy/get", "/reverse-proxy*", "/get"),
+ // boundary: only strip on a '/' (or end of string) boundary
+ Arguments.of("/reverse-proxyfoo", "/reverse-proxy",
"/reverse-proxyfoo"),
+ Arguments.of("/other/x", "/reverse-proxy", "/other/x"),
+ // REST-DSL {name} placeholder segment matches any single
non-empty segment
+ Arguments.of("/user/123/orders", "/user/{id}", "/orders"),
+ Arguments.of("/user/123", "/user/{id}", "/"),
+ // request shorter than consumer path never matches
+ Arguments.of("/reverse", "/reverse-proxy", "/reverse"),
+ Arguments.of("/", "/reverse-proxy/sub", "/"),
+ // "no prefix to strip" cases: consumer path of "/", "" or
null always returns input unchanged -
+ // this is what makes platform-http:proxy (whose getPath() is
"/") provably unaffected
+ Arguments.of("/reverse-proxy/get", "/", "/reverse-proxy/get"),
+ Arguments.of("/reverse-proxy/get", "", "/reverse-proxy/get"),
+ Arguments.of("/reverse-proxy/get", null, "/reverse-proxy/get"),
+ Arguments.of("/reverse-proxy/get", " ",
"/reverse-proxy/get"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("stripCases")
+ void stripsAsExpected(String requestPath, String consumerPath, String
expected) {
+ assertEquals(expected, HttpHelper.stripUriPrefix(requestPath,
consumerPath));
+ }
+
+ @Test
+ void nullRequestPathReturnsNull() {
+ assertNull(HttpHelper.stripUriPrefix(null, "/reverse-proxy"));
+ assertNull(HttpHelper.stripUriPrefix(null, "/"));
+ assertNull(HttpHelper.stripUriPrefix(null, null));
+ }
+}
diff --git a/components/camel-platform-http-vertx/pom.xml
b/components/camel-platform-http-vertx/pom.xml
index e3b1190aaef9..97e98671af4d 100644
--- a/components/camel-platform-http-vertx/pom.xml
+++ b/components/camel-platform-http-vertx/pom.xml
@@ -42,6 +42,10 @@
<groupId>org.apache.camel</groupId>
<artifactId>camel-platform-http</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-http-base</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-attachments</artifactId>
diff --git
a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
index 2744a63fb1d9..9fdaa9a5784e 100644
---
a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
+++
b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
@@ -54,6 +54,7 @@ import
org.apache.camel.component.platform.http.cookie.CookieHandler;
import org.apache.camel.component.platform.http.spi.Method;
import org.apache.camel.component.platform.http.spi.PlatformHttpConsumer;
import
org.apache.camel.component.platform.http.spi.PlatformHttpSecurityHandler;
+import org.apache.camel.http.base.HttpHelper;
import org.apache.camel.spi.HeaderFilterStrategy;
import org.apache.camel.spi.RestRegistry;
import org.apache.camel.spi.RestRegistry.RestService;
@@ -487,6 +488,10 @@ public class VertxPlatformHttpConsumer extends
DefaultConsumer
protected Future<Void> populateCamelMessage(RoutingContext ctx, Exchange
exchange, Message message) {
final HeaderFilterStrategy headerFilterStrategy =
getEndpoint().getHeaderFilterStrategy();
populateCamelHeaders(ctx, message.getHeaders(), exchange,
headerFilterStrategy);
+ if (getEndpoint().isStripUriPrefix()) {
+ String httpPath = (String) message.getHeader(Exchange.HTTP_PATH);
+ message.setHeader(Exchange.HTTP_PATH,
HttpHelper.stripUriPrefix(httpPath, getEndpoint().getPath()));
+ }
if (securityHandler != null) {
message.removeHeader(AUTHORIZATION);
}
diff --git
a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpStripUriPrefixTest.java
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpStripUriPrefixTest.java
new file mode 100644
index 000000000000..f89b3b9cb1b6
--- /dev/null
+++
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpStripUriPrefixTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.platform.http.vertx;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.AvailablePortFinder;
+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 com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static
com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.equalTo;
+
+/**
+ * Tests the stripUriPrefix consumer option, which lets a platform-http route
combined with the http producer's
+ * bridgeEndpoint option act as a path-based reverse proxy: the registered
consumer path is stripped from CamelHttpPath
+ * before the request reaches the route, so the backend only sees the path
relative to the consumer.
+ */
+public class VertxPlatformHttpStripUriPrefixTest {
+
+ @RegisterExtension
+ AvailablePortFinder.Port backendPort = AvailablePortFinder.find();
+ @RegisterExtension
+ AvailablePortFinder.Port camelPort = AvailablePortFinder.find();
+
+ private WireMockServer wireMockServer;
+
+ @BeforeEach
+ void before() {
+ wireMockServer = new
WireMockServer(options().port(backendPort.getPort()));
+ // stubbed as it should be received once the /reverse-proxy prefix has
been stripped
+ wireMockServer.stubFor(get(urlEqualTo("/get?arg1=val1"))
+ .willReturn(aResponse().withStatus(200).withBody("stripped")));
+ // stubbed as it should be received when stripUriPrefix is NOT enabled
(control)
+ wireMockServer.stubFor(get(urlEqualTo("/reverse-proxy/get?arg1=val1"))
+
.willReturn(aResponse().withStatus(200).withBody("unstripped")));
+ // stubbed for an exact (non-prefixed) match, which should strip down
to "/"
+ wireMockServer.stubFor(get(urlEqualTo("/"))
+ .willReturn(aResponse().withStatus(200).withBody("root")));
+ wireMockServer.start();
+ }
+
+ @AfterEach
+ void after() {
+ if (wireMockServer != null) {
+ wireMockServer.stop();
+ }
+ }
+
+ @Test
+ void stripUriPrefixRemovesTheConsumerPathBeforeBridging() throws Exception
{
+ final CamelContext context =
VertxPlatformHttpEngineTest.createCamelContext(camelPort.getPort());
+ try {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("platform-http:/reverse-proxy?matchOnUriPrefix=true&stripUriPrefix=true")
+ .to("http://localhost:" + backendPort.getPort() +
"?bridgeEndpoint=true");
+ }
+ });
+ context.start();
+
+ given()
+ .when().get("http://localhost:" + camelPort.getPort() +
"/reverse-proxy/get?arg1=val1")
+ .then()
+ .statusCode(200)
+ .body(equalTo("stripped"));
+ } finally {
+ context.stop();
+ }
+ }
+
+ @Test
+ void withoutStripUriPrefixTheFullPathIsForwardedUnchanged() throws
Exception {
+ final CamelContext context =
VertxPlatformHttpEngineTest.createCamelContext(camelPort.getPort());
+ try {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("platform-http:/reverse-proxy?matchOnUriPrefix=true")
+ .to("http://localhost:" + backendPort.getPort() +
"?bridgeEndpoint=true");
+ }
+ });
+ context.start();
+
+ given()
+ .when().get("http://localhost:" + camelPort.getPort() +
"/reverse-proxy/get?arg1=val1")
+ .then()
+ .statusCode(200)
+ .body(equalTo("unstripped"));
+ } finally {
+ context.stop();
+ }
+ }
+
+ @Test
+ void stripUriPrefixOnAnExactMatchLeavesTheRootPath() throws Exception {
+ // matchOnUriPrefix is not required for stripUriPrefix: on an exact
match the path is stripped to "/"
+ final CamelContext context =
VertxPlatformHttpEngineTest.createCamelContext(camelPort.getPort());
+ try {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("platform-http:/reverse-proxy?stripUriPrefix=true")
+ .to("http://localhost:" + backendPort.getPort() +
"?bridgeEndpoint=true");
+ }
+ });
+ context.start();
+
+ given()
+ .when().get("http://localhost:" + camelPort.getPort() +
"/reverse-proxy")
+ .then()
+ .statusCode(200)
+ .body(equalTo("root"));
+ } finally {
+ context.stop();
+ }
+ }
+}
diff --git
a/components/camel-platform-http/src/generated/java/org/apache/camel/component/platform/http/PlatformHttpEndpointConfigurer.java
b/components/camel-platform-http/src/generated/java/org/apache/camel/component/platform/http/PlatformHttpEndpointConfigurer.java
index aa5c1cb72e71..d546aa9ddbb2 100644
---
a/components/camel-platform-http/src/generated/java/org/apache/camel/component/platform/http/PlatformHttpEndpointConfigurer.java
+++
b/components/camel-platform-http/src/generated/java/org/apache/camel/component/platform/http/PlatformHttpEndpointConfigurer.java
@@ -65,6 +65,8 @@ public class PlatformHttpEndpointConfigurer extends
PropertyConfigurerSupport im
case "requestTimeout": target.setRequestTimeout(property(camelContext,
long.class, value)); return true;
case "returnhttprequestheaders":
case "returnHttpRequestHeaders":
target.setReturnHttpRequestHeaders(property(camelContext, boolean.class,
value)); return true;
+ case "stripuriprefix":
+ case "stripUriPrefix": target.setStripUriPrefix(property(camelContext,
boolean.class, value)); return true;
case "usebodyhandler":
case "useBodyHandler": target.setUseBodyHandler(property(camelContext,
boolean.class, value)); return true;
case "usecookiehandler":
@@ -120,6 +122,8 @@ public class PlatformHttpEndpointConfigurer extends
PropertyConfigurerSupport im
case "requestTimeout": return long.class;
case "returnhttprequestheaders":
case "returnHttpRequestHeaders": return boolean.class;
+ case "stripuriprefix":
+ case "stripUriPrefix": return boolean.class;
case "usebodyhandler":
case "useBodyHandler": return boolean.class;
case "usecookiehandler":
@@ -176,6 +180,8 @@ public class PlatformHttpEndpointConfigurer extends
PropertyConfigurerSupport im
case "requestTimeout": return target.getRequestTimeout();
case "returnhttprequestheaders":
case "returnHttpRequestHeaders": return
target.isReturnHttpRequestHeaders();
+ case "stripuriprefix":
+ case "stripUriPrefix": return target.isStripUriPrefix();
case "usebodyhandler":
case "useBodyHandler": return target.isUseBodyHandler();
case "usecookiehandler":
diff --git
a/components/camel-platform-http/src/generated/java/org/apache/camel/component/platform/http/PlatformHttpEndpointUriFactory.java
b/components/camel-platform-http/src/generated/java/org/apache/camel/component/platform/http/PlatformHttpEndpointUriFactory.java
index 4a7a05b1fcdb..e4a06590ddda 100644
---
a/components/camel-platform-http/src/generated/java/org/apache/camel/component/platform/http/PlatformHttpEndpointUriFactory.java
+++
b/components/camel-platform-http/src/generated/java/org/apache/camel/component/platform/http/PlatformHttpEndpointUriFactory.java
@@ -24,7 +24,7 @@ public class PlatformHttpEndpointUriFactory extends
org.apache.camel.support.com
private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
- Set<String> props = new HashSet<>(26);
+ Set<String> props = new HashSet<>(27);
props.add("bridgeErrorHandler");
props.add("consumes");
props.add("cookieDomain");
@@ -48,6 +48,7 @@ public class PlatformHttpEndpointUriFactory extends
org.apache.camel.support.com
props.add("produces");
props.add("requestTimeout");
props.add("returnHttpRequestHeaders");
+ props.add("stripUriPrefix");
props.add("useBodyHandler");
props.add("useCookieHandler");
props.add("useStreaming");
diff --git
a/components/camel-platform-http/src/generated/resources/META-INF/org/apache/camel/component/platform/http/platform-http.json
b/components/camel-platform-http/src/generated/resources/META-INF/org/apache/camel/component/platform/http/platform-http.json
index e2cdf3cf26fb..d9dc24f3a08c 100644
---
a/components/camel-platform-http/src/generated/resources/META-INF/org/apache/camel/component/platform/http/platform-http.json
+++
b/components/camel-platform-http/src/generated/resources/META-INF/org/apache/camel/component/platform/http/platform-http.json
@@ -50,15 +50,16 @@
"produces": { "index": 13, "kind": "parameter", "displayName": "Produces",
"group": "consumer", "label": "consumer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "description": "The content type this endpoint produces, such
as application\/xml or application\/json." },
"requestTimeout": { "index": 14, "kind": "parameter", "displayName":
"Request Timeout", "group": "consumer", "label": "advanced,consumer",
"required": false, "type": "integer", "javaType": "long", "deprecated": false,
"autowired": false, "secret": false, "description": "The period in milliseconds
after which the request should be timed out." },
"returnHttpRequestHeaders": { "index": 15, "kind": "parameter",
"displayName": "Return Http Request Headers", "group": "consumer", "label":
"advanced,consumer", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether to include HTTP request headers
(Accept, User-Agent, etc.) into HTTP response produced by this endpoint." },
- "useBodyHandler": { "index": 16, "kind": "parameter", "displayName": "Use
Body Handler", "group": "consumer", "label": "advanced,consumer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether to use BodyHandler for the request. If set to false then the request
will no be read and parsed." },
- "useCookieHandler": { "index": 17, "kind": "parameter", "displayName":
"Use Cookie Handler", "group": "consumer", "label": "advanced,consumer",
"required": false, "type": "boolean", "javaType": "boolean", "deprecated":
false, "autowired": false, "secret": false, "defaultValue": false,
"description": "Whether to enable the Cookie Handler that allows Cookie
addition, expiry, and retrieval (currently only supported by
camel-platform-http-vertx)" },
- "useStreaming": { "index": 18, "kind": "parameter", "displayName": "Use
Streaming", "group": "consumer", "label": "advanced,consumer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Whether to use streaming for large requests and responses (currently only
supported by camel-platform-http-vertx)" },
- "bridgeErrorHandler": { "index": 19, "kind": "parameter", "displayName":
"Bridge Error Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Allows for bridging the consumer to the
Camel routing Error Handler, which mean any exceptions (if possible) occurred
while the Camel consumer is trying to pickup incoming [...]
- "exceptionHandler": { "index": 20, "kind": "parameter", "displayName":
"Exception Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "object", "javaType":
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.",
"deprecated": false, "autowired": false, "secret": false, "description": "To
let the consumer use a custom ExceptionHandler. Notice if the option
bridgeErrorHandler is enabled then this option is not in use. By de [...]
- "exchangePattern": { "index": 21, "kind": "parameter", "displayName":
"Exchange Pattern", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "enum", "javaType":
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ],
"deprecated": false, "autowired": false, "secret": false, "description": "Sets
the exchange pattern when the consumer creates an exchange." },
- "fileNameExtWhitelist": { "index": 22, "kind": "parameter", "displayName":
"File Name Ext Whitelist", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"description": "A comma or whitespace separated list of file extensions.
Uploads having these extensions will be stored locally. Null value or asterisk
() will allow all files." },
- "headerFilterStrategy": { "index": 23, "kind": "parameter", "displayName":
"Header Filter Strategy", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType":
"org.apache.camel.spi.HeaderFilterStrategy", "deprecated": false, "autowired":
false, "secret": false, "description": "To use a custom HeaderFilterStrategy to
filter headers to and from Camel message." },
- "platformHttpEngine": { "index": 24, "kind": "parameter", "displayName":
"Platform Http Engine", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType":
"org.apache.camel.component.platform.http.spi.PlatformHttpEngine",
"deprecated": false, "autowired": false, "secret": false, "description": "An
HTTP Server engine implementation to serve the requests of this endpoint." },
- "oauthProfile": { "index": 25, "kind": "parameter", "displayName": "OAuth
Profile", "group": "security", "label": "consumer,security", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "OAuth profile name for
validating incoming Authorization: Bearer tokens. When set, the request is
authenticated before the route is processed. This requires an
OAuthTokenValidationFactory; camel-oauth provides [...]
+ "stripUriPrefix": { "index": 16, "kind": "parameter", "displayName":
"Strip Uri Prefix", "group": "consumer", "label": "consumer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Whether to strip the registered consumer path from CamelHttpPath after the
request has been matched, so the exchange sees the path relative to this
consumer instead of the full raw request path. Comb [...]
+ "useBodyHandler": { "index": 17, "kind": "parameter", "displayName": "Use
Body Handler", "group": "consumer", "label": "advanced,consumer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether to use BodyHandler for the request. If set to false then the request
will no be read and parsed." },
+ "useCookieHandler": { "index": 18, "kind": "parameter", "displayName":
"Use Cookie Handler", "group": "consumer", "label": "advanced,consumer",
"required": false, "type": "boolean", "javaType": "boolean", "deprecated":
false, "autowired": false, "secret": false, "defaultValue": false,
"description": "Whether to enable the Cookie Handler that allows Cookie
addition, expiry, and retrieval (currently only supported by
camel-platform-http-vertx)" },
+ "useStreaming": { "index": 19, "kind": "parameter", "displayName": "Use
Streaming", "group": "consumer", "label": "advanced,consumer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Whether to use streaming for large requests and responses (currently only
supported by camel-platform-http-vertx)" },
+ "bridgeErrorHandler": { "index": 20, "kind": "parameter", "displayName":
"Bridge Error Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Allows for bridging the consumer to the
Camel routing Error Handler, which mean any exceptions (if possible) occurred
while the Camel consumer is trying to pickup incoming [...]
+ "exceptionHandler": { "index": 21, "kind": "parameter", "displayName":
"Exception Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "object", "javaType":
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.",
"deprecated": false, "autowired": false, "secret": false, "description": "To
let the consumer use a custom ExceptionHandler. Notice if the option
bridgeErrorHandler is enabled then this option is not in use. By de [...]
+ "exchangePattern": { "index": 22, "kind": "parameter", "displayName":
"Exchange Pattern", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "enum", "javaType":
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ],
"deprecated": false, "autowired": false, "secret": false, "description": "Sets
the exchange pattern when the consumer creates an exchange." },
+ "fileNameExtWhitelist": { "index": 23, "kind": "parameter", "displayName":
"File Name Ext Whitelist", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"description": "A comma or whitespace separated list of file extensions.
Uploads having these extensions will be stored locally. Null value or asterisk
() will allow all files." },
+ "headerFilterStrategy": { "index": 24, "kind": "parameter", "displayName":
"Header Filter Strategy", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType":
"org.apache.camel.spi.HeaderFilterStrategy", "deprecated": false, "autowired":
false, "secret": false, "description": "To use a custom HeaderFilterStrategy to
filter headers to and from Camel message." },
+ "platformHttpEngine": { "index": 25, "kind": "parameter", "displayName":
"Platform Http Engine", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType":
"org.apache.camel.component.platform.http.spi.PlatformHttpEngine",
"deprecated": false, "autowired": false, "secret": false, "description": "An
HTTP Server engine implementation to serve the requests of this endpoint." },
+ "oauthProfile": { "index": 26, "kind": "parameter", "displayName": "OAuth
Profile", "group": "security", "label": "consumer,security", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "OAuth profile name for
validating incoming Authorization: Bearer tokens. When set, the request is
authenticated before the route is processed. This requires an
OAuthTokenValidationFactory; camel-oauth provides [...]
}
}
diff --git
a/components/camel-platform-http/src/main/docs/platform-http-component.adoc
b/components/camel-platform-http/src/main/docs/platform-http-component.adoc
index 1b5790d7d0f1..0b551804037e 100644
--- a/components/camel-platform-http/src/main/docs/platform-http-component.adoc
+++ b/components/camel-platform-http/src/main/docs/platform-http-component.adoc
@@ -201,6 +201,37 @@ Platform HTTP component can act as a reverse proxy. In
that case, some headers a
At this moment, this feature is only supported for Quarkus implemented in
`camel-platform-http-vertx` component.
+=== Forwarding requests under a path prefix (path-based reverse proxy)
+
+This is a different scenario from the `platform-http:proxy` forward proxy
described above, which is a `Host` header
+based forward proxy and only supported in `camel-platform-http-vertx`. Here,
the consumer is registered under an
+ordinary path, e.g. `/reverse-proxy`, and requests received under that path
should be forwarded to a fixed backend
+with the prefix stripped, using the xref:components::http-component.adoc[http]
producer's `bridgeEndpoint` option.
+
+Enable the consumer option `stripUriPrefix` so the matched consumer path is
removed from `CamelHttpPath` before the
+route runs:
+
+[source,java]
+----
+from("platform-http:/reverse-proxy?matchOnUriPrefix=true&stripUriPrefix=true")
+ .to("http://backend?bridgeEndpoint=true");
+----
+
+`matchOnUriPrefix` is not required for `stripUriPrefix`; it is only needed to
also forward requests under sub-paths.
+On an exact match (e.g. `GET /reverse-proxy`) the path is stripped to `/`.
+
+|===
+| Incoming request | `CamelHttpPath` (`stripUriPrefix=false`, default) |
`CamelHttpPath` (`stripUriPrefix=true`) | Forwarded to
+
+| `GET /reverse-proxy/get`
+| `/reverse-proxy/get`
+| `/get`
+| `http://backend/get`
+|===
+
+`CamelHttpUri`/`CamelHttpUrl` still hold the full original request, only
`CamelHttpPath` is rewritten. This option is
+currently implemented in `camel-platform-http-vertx`.
+
=== File Attachments handling
Since Apache Camel 4.10, multipart file uploads are easier and harmonized
across all runtimes. When a single file is uploaded, the Apache Camel framework
provides the following:
diff --git
a/components/camel-platform-http/src/main/java/org/apache/camel/component/platform/http/PlatformHttpEndpoint.java
b/components/camel-platform-http/src/main/java/org/apache/camel/component/platform/http/PlatformHttpEndpoint.java
index 8574380b9d6e..793027e5f4eb 100644
---
a/components/camel-platform-http/src/main/java/org/apache/camel/component/platform/http/PlatformHttpEndpoint.java
+++
b/components/camel-platform-http/src/main/java/org/apache/camel/component/platform/http/PlatformHttpEndpoint.java
@@ -104,6 +104,18 @@ public class PlatformHttpEndpoint extends DefaultEndpoint
description = "Whether or not the consumer should try to find a
target consumer "
+ "by matching the URI prefix if no exact match is
found.")
private boolean matchOnUriPrefix;
+ @UriParam(label = "consumer", defaultValue = "false",
+ description = "Whether to strip the registered consumer path
from CamelHttpPath after the request has been"
+ + " matched, so the exchange sees the path
relative to this consumer instead of the full raw"
+ + " request path. Combined with the http
producer's bridgeEndpoint option this allows building a"
+ + " path-based reverse proxy without manual header
manipulation, e.g. a route on"
+ + " platform-http:/reverse-proxy with
matchOnUriPrefix=true and stripUriPrefix=true bridged to"
+ + " http://backend forwards /reverse-proxy/get to
http://backend/get instead of"
+ + " http://backend/reverse-proxy/get. CamelHttpUri
and CamelHttpUrl are left untouched. The other"
+ + " HTTP consumers (camel-servlet, camel-jetty,
camel-netty-http, camel-undertow) already behave"
+ + " this way by default; this option brings
platform-http in line with them without changing its"
+ + " default behavior.")
+ private boolean stripUriPrefix;
@UriParam(label = "consumer", description = "A comma separated list of
HTTP methods to serve, e.g. GET,POST ."
+ " If no methods are
specified, all methods will be served.")
private String httpMethodRestrict;
@@ -271,6 +283,21 @@ public class PlatformHttpEndpoint extends DefaultEndpoint
this.matchOnUriPrefix = matchOnUriPrefix;
}
+ public boolean isStripUriPrefix() {
+ return stripUriPrefix;
+ }
+
+ /**
+ * If the option is true, the registered consumer path is stripped from
the CamelHttpPath header after the request
+ * has been matched, so the exchange sees the path relative to this
consumer instead of the full raw request path.
+ * This is useful in reverse proxy applications built with the http
producer's bridgeEndpoint option, where the
+ * downstream target should receive the path with this consumer's own
prefix removed. CamelHttpUri and CamelHttpUrl
+ * are left untouched.
+ */
+ public void setStripUriPrefix(boolean stripUriPrefix) {
+ this.stripUriPrefix = stripUriPrefix;
+ }
+
public String getHttpMethodRestrict() {
return httpMethodRestrict;
}
diff --git
a/components/camel-platform-http/src/test/java/org/apache/camel/component/platform/http/PlatformHttpEndpointStripUriPrefixTest.java
b/components/camel-platform-http/src/test/java/org/apache/camel/component/platform/http/PlatformHttpEndpointStripUriPrefixTest.java
new file mode 100644
index 000000000000..fd4957d56ad6
--- /dev/null
+++
b/components/camel-platform-http/src/test/java/org/apache/camel/component/platform/http/PlatformHttpEndpointStripUriPrefixTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.platform.http;
+
+import org.apache.camel.impl.DefaultCamelContext;
+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.assertTrue;
+
+/**
+ * The stripUriPrefix consumer option must be provably inert for
platform-http:proxy mode: proxy mode makes
+ * {@link PlatformHttpEndpoint#getPath()} return "/", and {@link
org.apache.camel.http.base.HttpHelper#stripUriPrefix}
+ * treats a consumer path of "/" as "no prefix to strip", so enabling
stripUriPrefix on a proxy endpoint must never
+ * change its behavior.
+ */
+class PlatformHttpEndpointStripUriPrefixTest {
+
+ @Test
+ void stripUriPrefixDefaultsToFalse() throws Exception {
+ assertFalse(stripUriPrefixOf("platform-http:/reverse-proxy"));
+
assertFalse(stripUriPrefixOf("platform-http:/reverse-proxy?matchOnUriPrefix=true"));
+ }
+
+ @Test
+ void stripUriPrefixCanBeEnabled() throws Exception {
+
assertTrue(stripUriPrefixOf("platform-http:/reverse-proxy?stripUriPrefix=true"));
+
assertTrue(stripUriPrefixOf("platform-http:/reverse-proxy?matchOnUriPrefix=true&stripUriPrefix=true"));
+ }
+
+ @Test
+ void proxyModeIsUnaffectedByStripUriPrefix() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ context.start();
+ PlatformHttpComponent component = new
PlatformHttpComponent(context);
+ PlatformHttpEndpoint endpoint
+ = (PlatformHttpEndpoint)
component.createEndpoint("platform-http:proxy?stripUriPrefix=true");
+
+ // the option is honored as a plain bean property...
+ assertTrue(endpoint.isStripUriPrefix());
+ // ...but proxy mode detection and the effective consumer path are
unaffected by it
+ assertTrue(endpoint.isHttpProxy());
+ assertEquals("/", endpoint.getPath());
+ }
+ }
+
+ private static boolean stripUriPrefixOf(String uri) throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ context.start();
+ PlatformHttpComponent component = new
PlatformHttpComponent(context);
+ return ((PlatformHttpEndpoint)
component.createEndpoint(uri)).isStripUriPrefix();
+ }
+ }
+}
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/PlatformHttpEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/PlatformHttpEndpointBuilderFactory.java
index 9eba4146a388..d955fcc8965a 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/PlatformHttpEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/PlatformHttpEndpointBuilderFactory.java
@@ -311,6 +311,60 @@ public interface PlatformHttpEndpointBuilderFactory {
doSetProperty("produces", produces);
return this;
}
+ /**
+ * Whether to strip the registered consumer path from CamelHttpPath
+ * after the request has been matched, so the exchange sees the path
+ * relative to this consumer instead of the full raw request path.
+ * Combined with the http producer's bridgeEndpoint option this allows
+ * building a path-based reverse proxy without manual header
+ * manipulation, e.g. a route on platform-http:/reverse-proxy with
+ * matchOnUriPrefix=true and stripUriPrefix=true bridged to
+ * http://backend forwards /reverse-proxy/get to http://backend/get
+ * instead of http://backend/reverse-proxy/get. CamelHttpUri and
+ * CamelHttpUrl are left untouched. The other HTTP consumers
+ * (camel-servlet, camel-jetty, camel-netty-http, camel-undertow)
+ * already behave this way by default; this option brings platform-http
+ * in line with them without changing its default behavior.
+ *
+ * The option is a: <code>boolean</code> type.
+ *
+ * Default: false
+ * Group: consumer
+ *
+ * @param stripUriPrefix the value to set
+ * @return the dsl builder
+ */
+ default PlatformHttpEndpointBuilder stripUriPrefix(boolean
stripUriPrefix) {
+ doSetProperty("stripUriPrefix", stripUriPrefix);
+ return this;
+ }
+ /**
+ * Whether to strip the registered consumer path from CamelHttpPath
+ * after the request has been matched, so the exchange sees the path
+ * relative to this consumer instead of the full raw request path.
+ * Combined with the http producer's bridgeEndpoint option this allows
+ * building a path-based reverse proxy without manual header
+ * manipulation, e.g. a route on platform-http:/reverse-proxy with
+ * matchOnUriPrefix=true and stripUriPrefix=true bridged to
+ * http://backend forwards /reverse-proxy/get to http://backend/get
+ * instead of http://backend/reverse-proxy/get. CamelHttpUri and
+ * CamelHttpUrl are left untouched. The other HTTP consumers
+ * (camel-servlet, camel-jetty, camel-netty-http, camel-undertow)
+ * already behave this way by default; this option brings platform-http
+ * in line with them without changing its default behavior.
+ *
+ * The option will be converted to a <code>boolean</code> type.
+ *
+ * Default: false
+ * Group: consumer
+ *
+ * @param stripUriPrefix the value to set
+ * @return the dsl builder
+ */
+ default PlatformHttpEndpointBuilder stripUriPrefix(String
stripUriPrefix) {
+ doSetProperty("stripUriPrefix", stripUriPrefix);
+ return this;
+ }
/**
* OAuth profile name for validating incoming Authorization: Bearer
* tokens. When set, the request is authenticated before the route is