shashank created CAMEL-25051:
--------------------------------

             Summary: Message headers since 4.21: a header set as content-type 
is stored and sent on as Content-Type (CaseInsensitiveMap replaces the key with 
a known key of another case)
                 Key: CAMEL-25051
                 URL: https://issues.apache.org/jira/browse/CAMEL-25051
             Project: Camel
          Issue Type: Bug
          Components: camel-core
            Reporter: shashank


CAMEL-23691 (4.21.0, PR #23766) replaced the 
{{TreeMap(String.CASE_INSENSITIVE_ORDER)}} behind {{CaseInsensitiveMap}}, the 
default message headers map, by a hash table 
({{core/camel-util/src/main/java/org/apache/camel/util/CaseInsensitiveMap.java}}).
 It also added deduplication of well-known keys: {{put}} stores 
{{deduplicateKey(key, hash)}} ({{:87-100}}), which returns the registered key 
when {{knownEntries[idx].equalsIgnoreCase(key)}}. {{DefaultHeadersMapFactory}} 
registers all {{Exchange}} constants ({{ExchangeConstantProvider.values()}}), 
among them {{Content-Type}}, {{Content-Length}}, {{Content-Encoding}}, 
{{Transfer-Encoding}}, {{breadcrumbId}} and every {{Camel...}} name. So the 
first put of a header whose name matches one of them ignoring case stores the 
constant's spelling: a header set or received as {{content-type}} is stored, 
iterated and sent on as {{Content-Type}}, and {{camelfilename}} becomes 
{{CamelFileName}}. A later put with another case only replaces the value.

The key case was meant to be kept:
* PR #23766 lists under "After": "Original key case preserved (first-put case 
wins, same as before)". It presents the deduplication as replacing deserialized 
keys that match a known constant with the canonical interned reference, which 
is a memory optimisation for equal strings.
* The class javadoc still says "A map that uses case insensitive keys, but 
preserves the original key cases" (CAMEL-8095).
* The 4.21 upgrade guide only mentions "header key deduplication" (in the 
camel-headersmap deprecation note).

The folding is written down in two places: the {{registerKnownKeys}} javadoc 
("matches one of these strings (case-insensitive)") and 
{{core/camel-core/src/test/java/org/apache/camel/util/CaseInsensitiveMapTest.java:662}}
 ({{testKnownKeyDeduplication}}, "case-insensitive dedup: different case should 
still map to canonical"). So it is deliberate in the implementation. It 
contradicts the stated contract and the PR's own description, though, and 
nobody discussed it in the JIRA or the PR review.

The case of a header name matters wherever a component copies the names as they 
are and the transport is case-sensitive. The Kafka path is checked in the code: 
{{KafkaRecordProcessor}} sets the record header names on the message as they 
are ({{in.setHeader(header.key(), ...)}}), 
{{KafkaProducer.getPropagatedHeaders}} / {{getRecordHeader}} send 
{{entry.getKey()}}, and {{KafkaHeaderFilterStrategy}} does not filter 
{{Content-Type}}. So a Kafka to Kafka route now forwards a {{content-type}} 
record header (for example a CloudEvents binary-mode message) as 
{{Content-Type}}, and a consumer that reads {{content-type}} no longer finds 
it. Other transports whose header names are case-sensitive are affected the 
same way. I did not run this against a Kafka broker. The renaming itself is 
reproduced below.

*Reproduction* (standalone program against main 65f315628, 
{{CaseInsensitiveKeyCaseE2E.java}}):
{noformat}
new CaseInsensitiveMap() in an application with a CamelContext:
  put content-type, content-length, camelfilename, x-custom
  keySet() -> [Content-Type, Content-Length, CamelFileName, x-custom]
  TreeMap(CASE_INSENSITIVE_ORDER), the map up to 4.20 -> [camelfilename, 
content-type]  (case kept)

template.sendBodyAndHeader("direct:in", "{}", "ce_id", "1") to
from("direct:in").setHeader("content-type", ...).setHeader("transfer-encoding", 
...)
    .setHeader("breadcrumbid", ...).setHeader("x-trace", ...).to("mock:out")
  header names at mock:out -> [ce_id, Content-Type, Transfer-Encoding, 
breadcrumbId, x-trace]
  expected                 -> [ce_id, content-type, transfer-encoding, 
breadcrumbid, x-trace]
get("CONTENT-TYPE") works (control).
{noformat}
A jqwik property ("keys keep their case", 
{{R5CaseInsensitiveMapProperties.keysKeepTheirCase}}) fails with 
{{content-type}}. A Lean model ({{R5CaseInsensitiveMap.lean}}) shows that 
whenever a known key is the first match of a key ignoring case and differs from 
it, {{put}} stores the other key ({{put_renames}}). It also proves that with an 
exact-match comparison {{put}} always stores the caller's key 
({{fix_preserves_key}}).

Affected: 4.21.0, 4.22.0 and main ({{deduplicateKey}} with 
{{equalsIgnoreCase}}, checked at the tags). The camel-4.18.x and camel-4.14.x 
branches still extend {{TreeMap}} and are not affected. If there is a 4.22.x 
maintenance line, please consider a backport.

A user who needs the old map right away can plug a {{TreeMap}}-based map 
through {{ExtendedCamelContext.setHeadersMapFactory}} (camel-headersmap itself 
was removed in 4.23).

*Proposed fix:* in {{deduplicateKey}}, reuse the known key only when it is 
equal ({{knownEntries[idx].equals(key)}}). This keeps the whole memory saving 
the deduplication is for (the same constant arriving as another {{String}} 
instance, for example after deserialization): a key in another case cannot 
share the canonical instance anyway, and the hash is computed from the key, so 
nothing else depends on it. Lookups stay case-insensitive and the first put 
still decides the key case. Update the {{registerKnownKeys}} javadoc and the 
second half of {{testKnownKeyDeduplication}} accordingly.

Compatibility, to be stated in the PR: from 4.21 to 4.22 some case-sensitive 
checks on header names started to match lower-case names by accident, and they 
go back to their 4.20 behaviour:
* {{CxfHeaderHelper.propagateCamelToCxf}} 
({{Exchange.CONTENT_TYPE.equals(entry.getKey())}}, and a case-sensitive 
{{HashMap}} for the name mapping): a header set as {{content-type}} is dropped 
by the CXF header filter again instead of being used as the CXF message content 
type.
* {{NettyHttpProducer.removeCamelHeaders}} ({{key.startsWith("Camel")}}): a 
response header named {{camelfoo}} is kept again.

Neither is a security filter: {{DefaultHeaderFilterStrategy}} filters 
{{Camel*}} case-insensitively.

_Filed with Claude Code on behalf of allthingssecurity._




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to