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