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

Reply via email to