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-spring-boot.git
The following commit(s) were added to refs/heads/main by this push:
new f77087c2964 CAMEL-24577: platform-http path variables follow the path
Spring matched
f77087c2964 is described below
commit f77087c2964145c64948423aa35265f1c83d7684
Author: croway <[email protected]>
AuthorDate: Wed Sep 2 15:01:37 2026 +0200
CAMEL-24577: platform-http path variables follow the path Spring matched
SpringBootPlatformHttpBinding evaluated the rest placeholders of the
consumer
path against getRawPath(request), which is the undecoded request URI with
the
context-path removed. Spring dispatched the request against the parsed
RequestPath, whose segments are percent-decoded and stripped of matrix
parameters, so the header could disagree with the path the request was
matched
on: /greeting/%61dmin set name to "%61dmin" while Spring matched "admin",
and
/greeting/name;v=1 set name to "name;v=1".
The placeholders are now evaluated against the segments Spring matched,
taken
from ServletRequestPathUtils.parse(request), which also aligns the starter
with
the vertx engine, where the path params are decoded. The path is parsed
rather
than read back from the request attribute Spring caches, because the
consumer
services the request on its own executor and the dispatch may already have
removed the attribute, which would make the value depend on timing.
getRawPath() is unchanged, so Exchange.HTTP_PATH still reports the raw path
and
the context-path handling of CAMEL-22116 and CAMEL-23191 is preserved.
Co-Authored-By: Claude Opus 5 <[email protected]>
---
.../src/main/docs/platform-http.adoc | 11 ++
.../springboot/SpringBootPlatformHttpBinding.java | 61 +++++++--
...ingBootPlatformHttpBindingPathVariableTest.java | 64 +++++++++
.../SpringBootPlatformHttpPathVariableTest.java | 152 +++++++++++++++++++++
4 files changed, 279 insertions(+), 9 deletions(-)
diff --git
a/components-starter/camel-platform-http-starter/src/main/docs/platform-http.adoc
b/components-starter/camel-platform-http-starter/src/main/docs/platform-http.adoc
index 24898a54bba..15053a6a48c 100644
---
a/components-starter/camel-platform-http-starter/src/main/docs/platform-http.adoc
+++
b/components-starter/camel-platform-http-starter/src/main/docs/platform-http.adoc
@@ -6,6 +6,17 @@
The Platform HTTP starter provides Spring Boot auto-configuration for the
Camel Platform HTTP component.
+== Path variables
+
+A consumer path may declare placeholders, such as
`platform-http:/greeting/{name}`, and the matched value is
+set as a message header named after the placeholder.
+
+The values are taken from the path Spring Boot matched the request against, so
they are percent-decoded and
+carry no matrix parameters. A request to `/greeting/John%20Doe;v=1` sets the
`name` header to `John Doe`.
+
+The `CamelHttpPath` header is not affected: it reports the raw request path,
with the servlet context-path
+removed.
+
== Undertow Access Log
You can enable Undertow access log to be managed by whatever logging library
you have in your camel application, you have to set the following parameters:
diff --git
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBinding.java
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBinding.java
index ac434bcda00..6c432da6769 100644
---
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBinding.java
+++
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBinding.java
@@ -43,8 +43,11 @@ import org.apache.camel.util.IOHelper;
import org.apache.camel.util.URISupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.http.server.PathContainer;
+import org.springframework.http.server.RequestPath;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import
org.springframework.web.multipart.support.StandardMultipartHttpServletRequest;
+import org.springframework.web.util.ServletRequestPathUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -73,20 +76,21 @@ public class SpringBootPlatformHttpBinding extends
DefaultHttpBinding {
protected void populateRequestParameters(HttpServletRequest request,
Message message) {
super.populateRequestParameters(request, message);
- String path = getRawPath(request);
+ PlatformHttpEndpoint endpoint = (PlatformHttpEndpoint)
message.getExchange().getFromEndpoint();
+ String consumerPath = endpoint.getPath();
+ if (consumerPath != null && consumerPath.startsWith("/")) {
+ consumerPath = consumerPath.substring(1);
+ }
+ if (consumerPath == null || !useRestMatching(consumerPath)) {
+ return;
+ }
+ String path = getMatchedPath(request);
// skip leading slash
if (path != null && path.startsWith("/")) {
path = path.substring(1);
}
if (path != null) {
- PlatformHttpEndpoint endpoint = (PlatformHttpEndpoint)
message.getExchange().getFromEndpoint();
- String consumerPath = endpoint.getPath();
- if (consumerPath != null && consumerPath.startsWith("/")) {
- consumerPath = consumerPath.substring(1);
- }
- if (useRestMatching(consumerPath)) {
- HttpHelper.evalPlaceholders(message.getHeaders(), path,
consumerPath);
- }
+ HttpHelper.evalPlaceholders(message.getHeaders(), path,
consumerPath);
}
}
@@ -94,6 +98,45 @@ public class SpringBootPlatformHttpBinding extends
DefaultHttpBinding {
return path.indexOf('{') > -1;
}
+ /**
+ * The request path to evaluate the rest placeholders of the consumer path
against.
+ * <p/>
+ * Spring matches the request against the {@link RequestPath} it parses
from the request, whose segments are
+ * percent-decoded and stripped of matrix parameters. Evaluating the
placeholders against that same path makes the
+ * headers agree with the path the request was actually matched on, and
matches the decoded values the vertx engine
+ * provides.
+ * <p/>
+ * The path is parsed again instead of reading the one Spring cached in
the request, because the request is serviced
+ * on another thread and the dispatch that cached it may already have
removed it by then.
+ *
+ * @param request the current request
+ * @return the path the placeholders are evaluated against
+ */
+ private String getMatchedPath(HttpServletRequest request) {
+ try {
+ // pathWithinApplication has the context-path (and any servlet
path prefix) removed, which is the
+ // same part getRawPath skips
+ return
toMatchedValue(ServletRequestPathUtils.parse(request).pathWithinApplication());
+ } catch (Exception e) {
+ LOG.debug("Cannot parse request path of {}, using the raw path
instead", request.getRequestURI(), e);
+ return getRawPath(request);
+ }
+ }
+
+ private static String toMatchedValue(PathContainer path) {
+ StringBuilder sb = new StringBuilder(path.value().length());
+ for (PathContainer.Element element : path.elements()) {
+ if (element instanceof PathContainer.PathSegment segment) {
+ // the decoded segment, without its matrix parameters
+ sb.append(segment.valueToMatch());
+ } else {
+ // separator
+ sb.append(element.value());
+ }
+ }
+ return sb.toString();
+ }
+
@Override
protected void populateAttachments(HttpServletRequest request, Message
message) {
// check if there is multipart files, if so will put it into
DataHandler
diff --git
a/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBindingPathVariableTest.java
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBindingPathVariableTest.java
new file mode 100644
index 00000000000..569d543a512
--- /dev/null
+++
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBindingPathVariableTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.springboot;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.platform.http.PlatformHttpComponent;
+import org.apache.camel.component.platform.http.PlatformHttpConstants;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * The consumer services the request on its own executor, so the path
variables must be resolved without relying on the
+ * request path the dispatch cached, which may already have been removed by
then.
+ */
+public class SpringBootPlatformHttpBindingPathVariableTest {
+
+ @Test
+ void pathVariablesAreResolvedWithoutACachedRequestPath() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+
context.getRegistry().bind(PlatformHttpConstants.PLATFORM_HTTP_ENGINE_NAME,
+ new SpringBootPlatformHttpEngine(8080));
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("platform-http:/greeting/{name}")
+
.transform().simple("${header.name}|${header.CamelHttpPath}");
+ }
+ });
+ context.start();
+
+ PlatformHttpComponent component =
context.getComponent("platform-http", PlatformHttpComponent.class);
+ SpringBootPlatformHttpConsumer consumer
+ = (SpringBootPlatformHttpConsumer)
component.getHttpEndpoints().iterator().next().getConsumer();
+
+ MockHttpServletRequest request = new MockHttpServletRequest("GET",
"/greeting/%61dmin;v=1");
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ consumer.service(request, response).get(20, TimeUnit.SECONDS);
+
+ assertEquals(200, response.getStatus());
+ // the placeholder is decoded and carries no matrix parameter,
CamelHttpPath stays raw
+ assertEquals("admin|/greeting/%61dmin;v=1",
response.getContentAsString());
+ }
+ }
+}
diff --git
a/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpPathVariableTest.java
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpPathVariableTest.java
new file mode 100644
index 00000000000..d7abf3c5a2a
--- /dev/null
+++
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpPathVariableTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.springboot;
+
+import io.restassured.RestAssured;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import
org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import
org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.firewall.StrictHttpFirewall;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.equalTo;
+
+/**
+ * Path variables must be taken from the path Spring matched the request
against, so a percent encoded segment or a
+ * segment carrying matrix parameters is reported decoded and without its
matrix parameters, as the vertx engine does.
+ */
+@EnableAutoConfiguration
+@CamelSpringBootTest
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+ classes = { CamelAutoConfiguration.class,
+ SpringBootPlatformHttpPathVariableTest.class,
+ SpringBootPlatformHttpPathVariableTest.TestConfiguration.class,
+ PlatformHttpComponentAutoConfiguration.class,
+ SpringBootPlatformHttpAutoConfiguration.class })
+public class SpringBootPlatformHttpPathVariableTest {
+
+ @Autowired
+ private Environment env;
+
+ @BeforeEach
+ void setUp() {
+ RestAssured.port = env.getRequiredProperty("local.server.port",
Integer.class);
+ }
+
+ @Configuration
+ public static class TestConfiguration {
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
+ http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
+ .csrf(csrf -> csrf.disable());
+ return http.build();
+ }
+
+ @Bean
+ public WebSecurityCustomizer allowMatrixParametersCustomizer() {
+ // the strict firewall rejects matrix parameters by default
+ StrictHttpFirewall firewall = new StrictHttpFirewall();
+ firewall.setAllowSemicolon(true);
+ return web -> web.httpFirewall(firewall);
+ }
+
+ @Bean
+ public RouteBuilder pathVariableRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("platform-http:/greeting/{name}")
+
.transform().simple("${header.name}|${header.CamelHttpPath}");
+
+ rest("/rest")
+ .get("/{name}").to("direct:restName");
+
+ from("direct:restName")
+ .setBody().simple("${header.name}");
+ }
+ };
+ }
+ }
+
+ @Test
+ public void testPlainPathVariable() {
+ given()
+ .when()
+ .get("/greeting/Camel")
+ .then()
+ .statusCode(200)
+ .body(equalTo("Camel|/greeting/Camel"));
+ }
+
+ @Test
+ public void testPercentEncodedPathVariable() {
+ // Spring matched /greeting/admin, so the header must be admin, while
CamelHttpPath stays the raw path
+ given()
+ .urlEncodingEnabled(false)
+ .when()
+ .get("/greeting/%61dmin")
+ .then()
+ .statusCode(200)
+ .body(equalTo("admin|/greeting/%61dmin"));
+ }
+
+ @Test
+ public void testPercentEncodedSpaceInPathVariable() {
+ given()
+ .urlEncodingEnabled(false)
+ .when()
+ .get("/greeting/John%20Doe")
+ .then()
+ .statusCode(200)
+ .body(equalTo("John Doe|/greeting/John%20Doe"));
+ }
+
+ @Test
+ public void testMatrixParameterInPathVariable() {
+ // Spring matched /greeting/name, the matrix parameter is not part of
the segment it matched
+ given()
+ .urlEncodingEnabled(false)
+ .when()
+ .get("/greeting/name;v=1")
+ .then()
+ .statusCode(200)
+ .body(equalTo("name|/greeting/name;v=1"));
+ }
+
+ @Test
+ public void testRestDslPercentEncodedPathVariable() {
+ given()
+ .urlEncodingEnabled(false)
+ .when()
+ .get("/rest/%61dmin")
+ .then()
+ .statusCode(200)
+ .body(equalTo("admin"));
+ }
+}