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 5365912c9616 CAMEL-25051: camel-util - CaseInsensitiveMap keeps the
key case of known header names (#26931)
5365912c9616 is described below
commit 5365912c96164b53b420ce57442c7951ad56817c
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Sep 29 14:40:31 2026 +0530
CAMEL-25051: camel-util - CaseInsensitiveMap keeps the key case of known
header names (#26931)
* CAMEL-25051: camel-util - CaseInsensitiveMap keeps the key case of known
header names
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/impl/DefaultMessageHeaderTest.java | 17 ++++
.../apache/camel/util/CaseInsensitiveMapTest.java | 95 +++++++++++++++++-----
.../org/apache/camel/util/CaseInsensitiveMap.java | 10 ++-
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 10 +++
4 files changed, 106 insertions(+), 26 deletions(-)
diff --git
a/core/camel-core/src/test/java/org/apache/camel/impl/DefaultMessageHeaderTest.java
b/core/camel-core/src/test/java/org/apache/camel/impl/DefaultMessageHeaderTest.java
index d52a896dd7c0..ac71891e43e9 100644
---
a/core/camel-core/src/test/java/org/apache/camel/impl/DefaultMessageHeaderTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/impl/DefaultMessageHeaderTest.java
@@ -22,6 +22,7 @@ import java.util.Map;
import java.util.Set;
import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.support.DefaultExchange;
import org.apache.camel.support.DefaultMessage;
@@ -50,6 +51,22 @@ public class DefaultMessageHeaderTest {
assertEquals("cheese", msg.getHeader("foo"));
}
+ @Test
+ public void testKnownHeaderNameKeepsKeyCase() {
+ Message msg = new DefaultMessage(camelContext);
+ // content-type and camelfilename have the same name as
Exchange.CONTENT_TYPE and Exchange.FILE_NAME ignoring case
+ msg.setHeader("content-type", "text/plain");
+ msg.setHeader("camelfilename", "a.txt");
+ msg.setHeader(Exchange.CONTENT_LENGTH, 12);
+
+ assertEquals(Set.of("content-type", "camelfilename",
"Content-Length"), Set.copyOf(msg.getHeaders().keySet()));
+ assertEquals("text/plain", msg.getHeader(Exchange.CONTENT_TYPE));
+ assertEquals("a.txt", msg.getHeader(Exchange.FILE_NAME));
+
+ Message copy = msg.copy();
+ assertEquals(Set.of("content-type", "camelfilename",
"Content-Length"), Set.copyOf(copy.getHeaders().keySet()));
+ }
+
@Test
public void testLookupCaseAgnostic() {
Message msg = new DefaultMessage(camelContext);
diff --git
a/core/camel-core/src/test/java/org/apache/camel/util/CaseInsensitiveMapTest.java
b/core/camel-core/src/test/java/org/apache/camel/util/CaseInsensitiveMapTest.java
index b5edbdddeba1..45f430d564de 100644
---
a/core/camel-core/src/test/java/org/apache/camel/util/CaseInsensitiveMapTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/util/CaseInsensitiveMapTest.java
@@ -32,6 +32,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
+import org.apache.camel.ExchangeConstantProvider;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -662,31 +663,81 @@ public class CaseInsensitiveMapTest {
public void testKnownKeyDeduplication() {
// Register known keys
CaseInsensitiveMap.registerKnownKeys(List.of("CamelCharsetName",
"CamelExchangeId", "breadcrumbId"));
+ try {
+ Map<String, Object> map = new CaseInsensitiveMap();
+
+ // simulate deserialized key (new String to guarantee a different
object)
+ String deserializedKey = new String("CamelCharsetName");
+ map.put(deserializedKey, "UTF-8");
+
+ // the stored key should be the canonical reference, not the
deserialized copy
+ Map.Entry<String, Object> entry = map.entrySet().iterator().next();
+ assertSame("CamelCharsetName", entry.getKey());
+ assertNotSame(deserializedKey, entry.getKey());
+
+ // a key with a different case is not replaced by the canonical
key, as the map preserves the key case
+ Map<String, Object> map2 = new CaseInsensitiveMap();
+ String lowerCaseKey = "camelcharsetname";
+ map2.put(lowerCaseKey, "UTF-8");
+ Map.Entry<String, Object> entry2 =
map2.entrySet().iterator().next();
+ assertSame(lowerCaseKey, entry2.getKey());
+ assertEquals("UTF-8", map2.get("CamelCharsetName"));
+
+ // non-registered key is stored as-is
+ String custom = new String("CustomHeader");
+ map.put(custom, "value");
+ for (Map.Entry<String, Object> e : map.entrySet()) {
+ if (e.getValue().equals("value")) {
+ assertSame(custom, e.getKey());
+ }
+ }
+ } finally {
+
CaseInsensitiveMap.registerKnownKeys(ExchangeConstantProvider.values());
+ }
+ }
- Map<String, Object> map = new CaseInsensitiveMap();
-
- // simulate deserialized key (new String to guarantee a different
object)
- String deserializedKey = new String("CamelCharsetName");
- map.put(deserializedKey, "UTF-8");
+ @Test
+ public void testKnownKeyKeepsKeyCase() {
+ CaseInsensitiveMap.registerKnownKeys(List.of("Content-Type",
"CamelFileName"));
+ try {
+ Map<String, Object> map = new CaseInsensitiveMap();
+ map.put("content-type", "text/plain");
+ map.put("camelfilename", "a.txt");
+ map.put(new String("Content-Length"), "12");
- // the stored key should be the canonical reference, not the
deserialized copy
- Map.Entry<String, Object> entry = map.entrySet().iterator().next();
- assertSame("CamelCharsetName", entry.getKey());
- assertNotSame(deserializedKey, entry.getKey());
-
- // case-insensitive dedup: different case should still map to canonical
- Map<String, Object> map2 = new CaseInsensitiveMap();
- map2.put("camelcharsetname", "UTF-8");
- Map.Entry<String, Object> entry2 = map2.entrySet().iterator().next();
- assertSame("CamelCharsetName", entry2.getKey());
-
- // non-registered key is stored as-is
- String custom = new String("CustomHeader");
- map.put(custom, "value");
- for (Map.Entry<String, Object> e : map.entrySet()) {
- if (e.getValue().equals("value")) {
- assertSame(custom, e.getKey());
+ assertEquals(List.of("content-type", "camelfilename",
"Content-Length"), new ArrayList<>(map.keySet()));
+ List<String> entryKeys = new ArrayList<>();
+ for (Map.Entry<String, Object> e : map.entrySet()) {
+ entryKeys.add(e.getKey());
}
+ assertEquals(List.of("content-type", "camelfilename",
"Content-Length"), entryKeys);
+
+ // lookups are still case-insensitive
+ assertEquals("text/plain", map.get("Content-Type"));
+ assertEquals("text/plain", map.get("CONTENT-TYPE"));
+ assertEquals("a.txt", map.get("CamelFileName"));
+ assertTrue(map.containsKey("Content-Type"));
+
+ // the first put decides the case: a later put with another case
only replaces the value
+ map.put("Content-Type", "application/json");
+ assertEquals(1,
map.keySet().stream().filter("content-type"::equalsIgnoreCase).count());
+ assertTrue(map.containsKey("content-type"));
+ assertEquals("content-type", map.keySet().iterator().next());
+ assertEquals("application/json", map.get("content-type"));
+
+ // an equal key still shares the canonical instance
+ Map<String, Object> map2 = new CaseInsensitiveMap();
+ String key = new String("Content-Type");
+ map2.put(key, "text/plain");
+ String stored = map2.keySet().iterator().next();
+ assertSame("Content-Type", stored);
+ assertNotSame(key, stored);
+
+ // also when copied from another map
+ Map<String, Object> copy = new CaseInsensitiveMap(map);
+ assertEquals(List.of("content-type", "camelfilename",
"Content-Length"), new ArrayList<>(copy.keySet()));
+ } finally {
+
CaseInsensitiveMap.registerKnownKeys(ExchangeConstantProvider.values());
}
}
diff --git
a/core/camel-util/src/main/java/org/apache/camel/util/CaseInsensitiveMap.java
b/core/camel-util/src/main/java/org/apache/camel/util/CaseInsensitiveMap.java
index 7dbbb49e1125..dda61c26fb09 100644
---
a/core/camel-util/src/main/java/org/apache/camel/util/CaseInsensitiveMap.java
+++
b/core/camel-util/src/main/java/org/apache/camel/util/CaseInsensitiveMap.java
@@ -56,9 +56,10 @@ public class CaseInsensitiveMap extends AbstractMap<String,
Object> implements S
private static volatile int knownMask;
/**
- * Registers a set of well-known header key strings for deduplication.
When a key passed to {@link #put} matches one
- * of these strings (case-insensitive), the canonical reference from this
set is stored instead of the caller's
- * string, reducing memory when many map instances carry the same headers
(e.g. deserialized exchanges).
+ * Registers a set of well-known header key strings for deduplication.
When a key passed to {@link #put} is equal to
+ * one of these strings, the canonical reference from this set is stored
instead of the caller's string, reducing
+ * memory when many map instances carry the same headers (e.g.
deserialized exchanges). A key that only differs in
+ * case is stored as given, as the map preserves the original key cases.
* <p/>
* This method is intended to be called once during framework startup.
*/
@@ -91,7 +92,8 @@ public class CaseInsensitiveMap extends AbstractMap<String,
Object> implements S
}
int idx = tbl[hash & knownMask];
while (idx != EMPTY) {
- if (knownEntries[idx].equalsIgnoreCase(key)) {
+ // only an equal key is replaced, so the case of the caller's key
is kept
+ if (knownEntries[idx].equals(key)) {
return knownEntries[idx];
}
idx = knownChainNext[idx];
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index c69525a6512d..c62c31eddd11 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -135,6 +135,16 @@ counted as inflight and its unit of work not done. The
same now happens when the
An exception thrown from the `after` method of an advice no longer replaces
the exception the exchange has already
failed with. Instead, it is added as a suppressed exception to the existing
exception.
+=== camel-core - message headers keep the case of their names
+
+A header whose name is the name of a Camel constant in another case, such as
`content-type` for
+`Exchange.CONTENT_TYPE` (`Content-Type`) or `camelfilename`, keeps the name it
was set with, as it did in Camel 4.20
+and older. From Camel 4.21 to 4.22 the header map stored such a header under
the name of the constant, so a header
+set or received as `content-type` was sent on as `Content-Type`. Looking up a
header is case-insensitive as before, and
+the first name a header is set with is the one that is kept. Code that
iterates the headers and compares their names
+case-sensitively, such as `Exchange.CONTENT_TYPE.equals(key)` or
`key.startsWith("Camel")`, no longer matches
+`content-type` or `camelfilename`, as before Camel 4.21.
+
=== @PropertyInject - an invalid property value is an error (Breaking change)
When a property injected with `@PropertyInject` has a value that cannot be
converted to the type of the field or