You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@camel.apache.org by "Claus Ibsen (Jira)" <ji...@apache.org> on 2026/09/29 09:11:00 UTC

[jira] [Resolved] (CAMEL-25051) 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)

     [ https://issues.apache.org/jira/browse/CAMEL-25051?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel ]

Claus Ibsen resolved CAMEL-25051.
---------------------------------
    Resolution: Fixed

Fixed by https://github.com/apache/camel/pull/26931 (merged to main, 4.23.0).

_Claude Code on behalf of davsclaus_

> 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
>            Priority: Minor
>
> 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)