This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new b8d785ce3607 CAMEL-25685: camel-jetty - map the query parameters when 
Jetty cannot read the request body parameters
b8d785ce3607 is described below

commit b8d785ce36078ceb80e5a5b5906b9518bc5be474
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Oct 16 20:28:58 2026 +0530

    CAMEL-25685: camel-jetty - map the query parameters when Jetty cannot read 
the request body parameters
    
    [CAMEL-25685](https://issues.apache.org/jira/browse/CAMEL-25685)
    
    `AttachmentHttpBinding.populateRequestParameters` maps the query parameters 
through `request.getParameterNames()`. Jetty 12 reads the body parameters and 
the query parameters together there, and throws for the whole request when it 
cannot read the body parameters. The `catch` around the loop only logs at 
DEBUG, so the exchange is routed without any query parameter and the client 
gets 200. Two requests that I measured take this path:
    
    - a form-urlencoded POST with a body over Jetty's `maxFormContentSize` 
(200000 bytes by default), default options. The form fields are still mapped 
(Camel parses them from the body in `readFormUrlEncodedBody`); only the query 
parameters are lost. Example: `POST /upload?customerId=42` with a 250 KB form 
gives `header("customerId") == null`;
    - any multipart request on an endpoint with `enableMultipartFilter=false`, 
whatever its size. The catch was written for this case; the query parameters 
were dropped together with the multipart body parameters.
    
    Closes #27924
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../component/jetty12/AttachmentHttpBinding.java   |  50 +++++++-
 ...tpBindingQueryParametersUnreadableBodyTest.java | 134 +++++++++++++++++++++
 2 files changed, 178 insertions(+), 6 deletions(-)

diff --git 
a/components/camel-jetty/src/main/java/org/apache/camel/component/jetty12/AttachmentHttpBinding.java
 
b/components/camel-jetty/src/main/java/org/apache/camel/component/jetty12/AttachmentHttpBinding.java
index 608479ed8489..ec2ca384c6b8 100644
--- 
a/components/camel-jetty/src/main/java/org/apache/camel/component/jetty12/AttachmentHttpBinding.java
+++ 
b/components/camel-jetty/src/main/java/org/apache/camel/component/jetty12/AttachmentHttpBinding.java
@@ -21,6 +21,7 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Collection;
 import java.util.Enumeration;
+import java.util.List;
 import java.util.Map;
 
 import jakarta.activation.DataHandler;
@@ -37,6 +38,8 @@ import org.apache.camel.attachment.DefaultAttachmentMessage;
 import org.apache.camel.component.jetty.MultiPartFilter;
 import org.apache.camel.http.common.DefaultHttpBinding;
 import org.apache.camel.http.common.HttpHelper;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.UrlEncoded;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -119,8 +122,9 @@ final class AttachmentHttpBinding extends 
DefaultHttpBinding {
         // attachment is optional
         AttachmentMessage am = new DefaultAttachmentMessage(message);
 
+        Enumeration<?> names = null;
         try {
-            Enumeration<?> names = request.getParameterNames();
+            names = request.getParameterNames();
             while (names.hasMoreElements()) {
                 String name = (String) names.nextElement();
                 if (am.getAttachment(name) != null) {
@@ -162,11 +166,45 @@ final class AttachmentHttpBinding extends 
DefaultHttpBinding {
                 }
             }
         } catch (Exception e) {
-            // In Jetty 12.1.0+, getParameterNames() may fail for multipart 
requests
-            // when multipart filter is disabled. This is expected behavior 
when
-            // enableMultipartFilter=false is used, so we silently skip 
parameter processing
-            if (LOG.isDebugEnabled()) {
-                LOG.debug("Failed to populate request parameters, likely due 
to multipart parsing failure: {}", e.getMessage());
+            // In Jetty 12.1.0+, getParameterNames() reads the query and the 
body parameters together and fails for the
+            // whole request when it cannot read the body parameters, for 
example a multipart request when the
+            // multipart filter is disabled (enableMultipartFilter=false), or 
a form larger than Jetty's
+            // maxFormContentSize. The body parameters are skipped (a form 
body is mapped by readFormUrlEncodedBody).
+            if (names == null) {
+                // nothing has been mapped yet, and the query does not depend 
on the body, so map its parameters
+                LOG.debug("Failed to read the request parameters, mapping the 
query parameters only: {}", e.getMessage());
+                populateQueryParameters(request, message);
+            } else if (LOG.isDebugEnabled()) {
+                LOG.debug("Failed to populate request parameters: {}", 
e.getMessage());
+            }
+        }
+    }
+
+    private void populateQueryParameters(HttpServletRequest request, Message 
message) {
+        // decoded as UTF-8, which is Jetty's default query encoding
+        String query = request.getQueryString();
+        if (query == null || query.isEmpty()) {
+            return;
+        }
+        MultiMap<String> parameters;
+        try {
+            parameters = UrlEncoded.decodeQuery(query);
+        } catch (Exception e) {
+            LOG.debug("Failed to populate query parameters: {}", 
e.getMessage());
+            return;
+        }
+        Map<String, Object> headers = message.getHeaders();
+        for (Map.Entry<String, List<String>> entry : parameters.entrySet()) {
+            String name = entry.getKey();
+            // Avoid potential injections
+            String[] sanitizedValues = 
HttpHelper.sanitizeLog(entry.getValue().toArray(new String[0]));
+            for (String value : sanitizedValues) {
+                // use http helper to extract parameter value as it may 
contain multiple values
+                Object extracted = HttpHelper.extractHttpParameterValue(value);
+                if (getHeaderFilterStrategy() != null
+                        && 
!getHeaderFilterStrategy().applyFilterToExternalHeaders(name, extracted, 
message.getExchange())) {
+                    HttpHelper.appendHeader(headers, name, value);
+                }
             }
         }
     }
diff --git 
a/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/HttpBindingQueryParametersUnreadableBodyTest.java
 
b/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/HttpBindingQueryParametersUnreadableBodyTest.java
new file mode 100644
index 000000000000..55fca302597a
--- /dev/null
+++ 
b/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/HttpBindingQueryParametersUnreadableBodyTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.jetty;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Message;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+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.assertNull;
+
+/**
+ * A form that is larger than Jetty's maxFormContentSize (200000 bytes by 
default), or a multipart request with the
+ * multipart filter disabled, makes Jetty fail to read the request parameters. 
The query parameters must still be
+ * mapped.
+ */
+public class HttpBindingQueryParametersUnreadableBodyTest extends 
BaseJettyTest {
+
+    // larger than the default maxFormContentSize of Jetty
+    private static final String LARGE_VALUE = "x".repeat(250_000);
+
+    @Test
+    public void queryParametersOfLargeFormAreMapped() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:form");
+        mock.expectedMessageCount(1);
+
+        int status = post("?id=42&tag=a&tag=b&text=hello+world", 
"name=camel&data=" + LARGE_VALUE);
+
+        mock.assertIsSatisfied();
+        assertEquals(200, status);
+        Message in = mock.getReceivedExchanges().get(0).getIn();
+        assertEquals("42", in.getHeader("id"));
+        assertEquals(List.of("a", "b"), in.getHeader("tag"));
+        assertEquals("hello world", in.getHeader("text"));
+        // the form is mapped from the body as before
+        assertEquals("camel", in.getHeader("name"));
+        assertEquals(LARGE_VALUE.length(), in.getHeader("data", 
String.class).length());
+    }
+
+    @Test
+    public void queryParametersOfLargeFormAreFiltered() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:form");
+        mock.expectedMessageCount(1);
+
+        int status = post("?id=42&CamelHttpResponseCode=500", "data=" + 
LARGE_VALUE);
+
+        mock.assertIsSatisfied();
+        assertEquals(200, status);
+        Message in = mock.getReceivedExchanges().get(0).getIn();
+        assertEquals("42", in.getHeader("id"));
+        assertFalse(in.getHeaders().containsKey(Exchange.HTTP_RESPONSE_CODE));
+    }
+
+    @Test
+    public void invalidQueryOfLargeFormIsNotMapped() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:form");
+        mock.expectedMessageCount(1);
+
+        int status = post("?id=%E9", "data=" + LARGE_VALUE);
+
+        mock.assertIsSatisfied();
+        assertEquals(200, status);
+        assertNull(mock.getReceivedExchanges().get(0).getIn().getHeader("id"));
+    }
+
+    @Test
+    public void queryParametersOfMultipartWithoutFilterAreMapped() throws 
Exception {
+        MockEndpoint mock = getMockEndpoint("mock:multipart");
+        mock.expectedMessageCount(1);
+
+        String body = "--boundary\r\nContent-Disposition: form-data; 
name=\"name\"\r\n\r\ncamel\r\n--boundary--\r\n";
+        int status = send("/multipart?id=42", "multipart/form-data; 
boundary=boundary", body);
+
+        mock.assertIsSatisfied();
+        assertEquals(200, status);
+        Message in = mock.getReceivedExchanges().get(0).getIn();
+        assertEquals("42", in.getHeader("id"));
+        // the multipart body is left to the route, as before
+        assertEquals(body, in.getBody(String.class));
+    }
+
+    private int post(String query, String form) throws Exception {
+        return send("/form" + query, "application/x-www-form-urlencoded", 
form);
+    }
+
+    private int send(String pathAndQuery, String contentType, String body) 
throws Exception {
+        HttpRequest request = 
HttpRequest.newBuilder(URI.create("http://localhost:"; + getPort() + 
pathAndQuery))
+                .header("Content-Type", contentType)
+                .POST(HttpRequest.BodyPublishers.ofString(body, 
StandardCharsets.UTF_8))
+                .build();
+        return HttpClient.newHttpClient().send(request, 
HttpResponse.BodyHandlers.discarding()).statusCode();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from("jetty:http://localhost:{{port}}/form";)
+                        .to("mock:form")
+                        .removeHeaders("*")
+                        .setBody(constant("OK"));
+                
from("jetty:http://localhost:{{port}}/multipart?enableMultipartFilter=false";)
+                        .convertBodyTo(String.class)
+                        .to("mock:multipart")
+                        .removeHeaders("*")
+                        .setBody(constant("OK"));
+            }
+        };
+    }
+}

Reply via email to