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 f3a04698c7b8 CAMEL-25563: camel-http - Send each value of a
multi-value header as its own HTTP header (#27787)
f3a04698c7b8 is described below
commit f3a04698c7b83bc5a969e58c766ae89300fc96f6
Author: Urmila Unni <[email protected]>
AuthorDate: Wed Oct 14 17:16:59 2026 +0530
CAMEL-25563: camel-http - Send each value of a multi-value header as its
own HTTP header (#27787)
Co-authored-by: Claude <[email protected]>
---
.../apache/camel/component/http/HttpProducer.java | 46 ++++++--
.../http/HttpProducerMultiValueHeaderTest.java | 127 +++++++++++++++++++++
.../HttpProducerTwoHeadersWithSameKeyTest.java | 8 +-
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 15 +++
4 files changed, 184 insertions(+), 12 deletions(-)
diff --git
a/components/camel-http/src/main/java/org/apache/camel/component/http/HttpProducer.java
b/components/camel-http/src/main/java/org/apache/camel/component/http/HttpProducer.java
index c689b99da43d..a15243f1d4eb 100644
---
a/components/camel-http/src/main/java/org/apache/camel/component/http/HttpProducer.java
+++
b/components/camel-http/src/main/java/org/apache/camel/component/http/HttpProducer.java
@@ -35,6 +35,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.stream.Stream;
import org.apache.camel.CamelExchangeException;
import org.apache.camel.Exchange;
@@ -211,11 +212,18 @@ public class HttpProducer extends DefaultProducer
implements LineNumberAware {
continue;
}
- // use an iterator as there can be multiple values.
(must not use a delimiter, and allow empty values)
- final Iterator<?> it =
ObjectHelper.createIterator(headerValue, null, true);
-
- HttpUtil.applyHeader(strategy, exchange, it, tc, key,
- (multiValues, prev) ->
applyHeader(httpRequest, key, multiValues, prev));
+ if (isSentAsOneJoinedHeader(headerValue)) {
+ // a primitive array (other than byte[] and
char[]), an Iterator, a Stream, or an Iterable
+ // that is not a Collection is sent as one header
in the form [a, b], as before
+ final Iterator<?> it =
ObjectHelper.createIterator(headerValue, null, true);
+ HttpUtil.applyHeader(strategy, exchange, it, tc,
key,
+ (multiValues, prev) ->
applyJoinedHeader(httpRequest, key, multiValues, prev));
+ } else {
+ // a collection or an object array is sent as one
header per value; any other value
+ // is converted to one String and sent as a single
header
+ HttpUtil.applyHeaderValue(strategy, exchange,
headerValue, tc, key,
+ (multiValues, prev) ->
applyHeader(httpRequest, key, multiValues, prev));
+ }
}
}
}
@@ -340,12 +348,30 @@ public class HttpProducer extends DefaultProducer
implements LineNumberAware {
}
private static void applyHeader(HttpUriRequest httpRequest, String key,
List<String> multiValues, String prev) {
- // add the value(s) as a http request header
+ // add the value(s) as http request header(s), one header line per
value
+ if (multiValues != null) {
+ for (String value : multiValues) {
+ httpRequest.addHeader(key, value);
+ }
+ } else if (prev != null) {
+ httpRequest.addHeader(key, prev);
+ }
+ }
+
+ private static boolean isSentAsOneJoinedHeader(Object value) {
+ if (value instanceof Iterator || value instanceof Stream
+ || (value instanceof Iterable && !(value instanceof
Collection))) {
+ return true;
+ }
+ Class<?> type = value.getClass();
+ return type.isArray() && type.getComponentType().isPrimitive()
+ && type != byte[].class && type != char[].class;
+ }
+
+ private static void applyJoinedHeader(HttpUriRequest httpRequest, String
key, List<String> multiValues, String prev) {
+ // two or more values are sent as one header in the form [a, b], one
value as it is
if (multiValues != null) {
- // use the default toString of a ArrayList to create in the form
[xxx, yyy]
- // if multi valued, for a single value, then just output the value
as is
- String s = multiValues.size() > 1 ? multiValues.toString() :
multiValues.get(0);
- httpRequest.addHeader(key, s);
+ httpRequest.addHeader(key, multiValues.toString());
} else if (prev != null) {
httpRequest.addHeader(key, prev);
}
diff --git
a/components/camel-http/src/test/java/org/apache/camel/component/http/HttpProducerMultiValueHeaderTest.java
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpProducerMultiValueHeaderTest.java
new file mode 100644
index 000000000000..72e9dca208cf
--- /dev/null
+++
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpProducerMultiValueHeaderTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.http;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
+import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class HttpProducerMultiValueHeaderTest extends BaseHttpTest {
+
+ private HttpServer localServer;
+
+ @Override
+ public void setupResources() throws Exception {
+ localServer = ServerBootstrap.bootstrap()
+
.setCanonicalHostName("localhost").setHttpProcessor(getBasicHttpProcessor())
+
.setConnectionReuseStrategy(getConnectionReuseStrategy()).setResponseFactory(getHttpResponseFactory())
+ .setSslContext(getSSLContext())
+ .register("/myapp", (request, response, context) -> {
+ // reply with the values of the received header lines,
separated by |
+ String body = Arrays.stream(request.getHeaders("test"))
+ .map(Header::getValue)
+ .collect(Collectors.joining("|"));
+ response.setEntity(new StringEntity(body,
StandardCharsets.US_ASCII));
+ response.setCode(HttpStatus.SC_OK);
+ }).create();
+ localServer.start();
+ }
+
+ @Override
+ public void cleanupResources() throws Exception {
+ if (localServer != null) {
+ localServer.stop();
+ }
+ }
+
+ @Test
+ public void testListIsOneHeaderPerValue() {
+ assertEquals("a|b|c", send(List.of("a", "b", "c")));
+ }
+
+ @Test
+ public void testArrayIsOneHeaderPerValue() {
+ assertEquals("a|b", send(new String[] { "a", "b" }));
+ }
+
+ @Test
+ public void testStringWithCommaIsOneHeader() {
+ assertEquals("a, b", send("a, b"));
+ }
+
+ @Test
+ public void testNullElementsAreSkipped() {
+ assertEquals("a|b", send(Arrays.asList("a", null, "b")));
+ }
+
+ @Test
+ public void testSingleElementListIsOneHeader() {
+ assertEquals("x", send(List.of("x")));
+ }
+
+ @Test
+ public void testIntArrayIsOneHeaderAsBefore() {
+ assertEquals("[1, 2]", send(new int[] { 1, 2 }));
+ }
+
+ @Test
+ public void testIteratorIsOneHeaderAsBefore() {
+ assertEquals("[a, b]", send(List.of("a", "b").iterator()));
+ }
+
+ @Test
+ public void testByteArrayIsSentAsText() {
+ assertEquals("abc", send("abc".getBytes(StandardCharsets.US_ASCII)));
+ }
+
+ @Test
+ public void testStreamIsOneHeaderAsBefore() {
+ assertEquals("[a, b]", send(Stream.of("a", "b")));
+ }
+
+ @Test
+ public void testCharArrayIsSentAsText() {
+ assertEquals("ab", send(new char[] { 'a', 'b' }));
+ }
+
+ @Test
+ public void testIterableIsOneHeaderAsBefore() {
+ Iterable<String> iterable = () -> List.of("a", "b").iterator();
+ assertEquals("[a, b]", send(iterable));
+ }
+
+ @Test
+ public void testSingleElementIntArrayIsOneHeader() {
+ assertEquals("7", send(new int[] { 7 }));
+ }
+
+ private String send(Object headerValue) {
+ String uri = "http://localhost:" + localServer.getLocalPort() +
"/myapp";
+ return template.requestBodyAndHeader(uri, null, "test", headerValue,
String.class);
+ }
+}
diff --git
a/components/camel-http/src/test/java/org/apache/camel/component/http/HttpProducerTwoHeadersWithSameKeyTest.java
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpProducerTwoHeadersWithSameKeyTest.java
index 7f47d0d13b67..3793b976ea61 100644
---
a/components/camel-http/src/test/java/org/apache/camel/component/http/HttpProducerTwoHeadersWithSameKeyTest.java
+++
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpProducerTwoHeadersWithSameKeyTest.java
@@ -49,7 +49,9 @@ public class HttpProducerTwoHeadersWithSameKeyTest extends
BaseHttpTest {
Header[] from = request.getHeaders("from");
assertEquals("me", from[0].getValue());
Header[] to = request.getHeaders("to");
- assertEquals("[foo, bar]", to[0].getValue());
+ assertEquals(2, to.length);
+ assertEquals("foo", to[0].getValue());
+ assertEquals("bar", to[1].getValue());
response.setHeader("bar", "yes");
response.addHeader("foo", "123");
@@ -61,7 +63,9 @@ public class HttpProducerTwoHeadersWithSameKeyTest extends
BaseHttpTest {
Header[] from = request.getHeaders("from");
assertEquals("me", from[0].getValue());
Header[] to = request.getHeaders("to");
- assertEquals("[foo, bar]", to[0].getValue());
+ assertEquals(2, to.length);
+ assertEquals("foo", to[0].getValue());
+ assertEquals("bar", to[1].getValue());
response.setHeader("bar", "yes");
response.addHeader("foo", "123");
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index a67107184f73..afd11a244abb 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -243,6 +243,21 @@ of a `JsonArray`.
A route that needs the old single header of the producer can set the header to
a String, for example with
`String.valueOf(list)`.
+=== camel-http - a header with several values is sent as several headers
+
+A message header whose value is a `java.util.Collection` or an object array
(as the HTTP consumers map a request
+header that occurs more than once) was sent as one HTTP header with the values
in the form `[a, b]`. The producer now
+sends one HTTP header per value, as camel-vertx-http and camel-knative-http
do. Null values are skipped, and a
+collection with one value is sent as one header with that value, as before.
+
+Every other value is sent as one header. A String is sent as before, also when
it contains commas. A `byte[]` or a
+`char[]` is now converted to a String with the type converter, for example
`abc` (it was sent as `[97, 98, 99]`), and
+a `Map` is sent as its `toString()`, such as `{k1=v1, k2=v2}` (it was `[k1=v1,
k2=v2]`). The other primitive arrays,
+an `Iterator`, a `java.util.stream.Stream` and an `Iterable` that is not a
collection are sent in the form `[a, b]` as before. A route that needs
+the old single header can set the header to a String, for example with
`String.valueOf(list)`.
+
+This also applies to camel-graphql, which sends its requests with camel-http.
+
=== camel-dapr - the saveBulk and executeTransaction state operations are sent
The `saveBulk` and `executeTransaction` operations of `dapr:state` built their
request but never sent it to Dapr, so