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"));
+    }
+}

Reply via email to