shashank created CAMEL-25059:
--------------------------------
Summary: CaseInsensitiveMap (message headers) since 4.21:
keySet().remove(o) and removeAll(c) are case-sensitive, so they no longer
remove a header stored with another case
Key: CAMEL-25059
URL: https://issues.apache.org/jira/browse/CAMEL-25059
Project: Camel
Issue Type: Bug
Components: camel-core
Reporter: shashank
CAMEL-23691 (4.21.0) replaced the {{TreeMap}} with
{{String.CASE_INSENSITIVE_ORDER}} behind {{CaseInsensitiveMap}}, the default
message headers map, by a hash table that extends {{AbstractMap}}
({{core/camel-util/src/main/java/org/apache/camel/util/CaseInsensitiveMap.java}}).
The class overrides {{entrySet()}} but not {{keySet()}}. The key set of
{{AbstractMap}} routes {{contains}} to {{containsKey}}, which is
case-insensitive. It does not override {{remove}}, though, so
{{AbstractCollection.remove}} walks the keys and compares them with {{equals}},
which is case-sensitive. {{AbstractSet.removeAll(c)}} calls that {{remove}} for
each element of {{c}} when the set is larger than {{c}}.
The message headers expose this view: the key set of {{CopyOnWriteHeadersMap}}
({{core/camel-support/src/main/java/org/apache/camel/support/CopyOnWriteHeadersMap.java}})
delegates {{remove}}, {{removeAll}} and {{retainAll}} to it. So:
* {{exchange.getMessage().getHeaders().keySet().remove("x-trace")}} returns
{{false}} and leaves {{X-Trace}} in place.
* {{keySet().removeAll(List.of("x-trace", "x-tenant"))}} removes nothing.
{{Map.remove("x-trace")}} and {{keySet().contains("x-trace")}} work. A key set
is a view of the map ("removal from the set removes the corresponding
mapping"), and the keys of this map compare case-insensitively everywhere else.
With the old {{TreeMap}}, {{keySet().remove(o)}} went to {{TreeMap.remove}},
which is case-insensitive.
The effect is silent: code that removes headers through the key set keeps a
header when the sender used another case, and header names from the outside
arrive in any case (HTTP/2 lower-cases them, for example). That includes code
that strips sensitive headers this way before it calls another system. Camel's
own code does not remove headers through the key set: a {{git grep}} for
{{keySet().remove}}, {{removeAll}}, {{retainAll}} and {{removeIf}} over the
non-test code finds, on message headers, only the {{CopyOnWriteHeadersMap}}
delegation. {{removeHeader}}, {{removeHeaders(pattern)}}, {{Map.remove}} and
the header filter strategies are case-insensitive and not affected.
h3. Reproduction
A standalone program against main 65f315628, with the same calls on a
{{TreeMap(String.CASE_INSENSITIVE_ORDER)}} as the 4.20 control:
{noformat}
CaseInsensitiveMap with X-Trace, X-Tenant and Content-Type:
keySet().remove("x-trace") -> false, X-Trace still there
keySet().removeAll([x-trace, x-tenant]) -> keys left [X-Trace, X-Tenant,
Content-Type]
route: .process(e ->
e.getMessage().getHeaders().keySet().removeAll(Arrays.asList("x-trace",
"x-tenant")))
headers after -> [X-Trace, X-Tenant, Accept]
expected [Accept]
route: .process(e -> e.getMessage().getHeaders().keySet().remove("x-trace"))
headers after -> [X-Trace] expected []
same calls on TreeMap(CASE_INSENSITIVE_ORDER), the map up to 4.20 -> removed
controls: keySet().contains("x-trace") and Map.remove("x-trace") behave as with
the TreeMap
{noformat}
Note on 4.20: the {{removeAll(c)}} of the {{TreeMap}} key set was
size-dependent too. When {{c}} has at least as many elements as the map,
{{AbstractSet.removeAll}} iterates the map and asks {{c.contains(key)}}, which
is case-sensitive for a {{List}}. So {{removeAll([x-trace, x-tenant,
x-other])}} on a map with {{X-Trace}} and {{X-Tenant}} removed nothing on 4.20
either. {{retainAll(c)}} always used {{c.contains(key)}}, so it was
case-sensitive on 4.20 and is on main too. The regression in 4.21 is
{{remove(o)}}, and {{removeAll(c)}} with a smaller {{c}}.
A Lean model of the key set proves that {{keySet().remove(o)}} changes nothing
whenever no key is exactly {{o}}, whatever case-insensitive matches exist, and
a jqwik property ("keySet().remove works like TreeMap") fails with a stored
{{Transfer-Encoding}} and a removed {{transfer-encoding}}.
Related: CAMEL-25051 (known header names lose their key case since 4.21) is
another difference to the {{TreeMap}} in the same class; the two changes are
independent.
Affected: 4.21.0, 4.22.0 and main. The camel-4.18.x and camel-4.14.x branches
still extend {{TreeMap}} and are not affected.
h3. Proposed fix
Override {{keySet()}} in {{CaseInsensitiveMap}} with a small view that uses the
lookup of the map:
* {{size}}, and {{contains}} via {{containsKey}};
* {{remove(o)}} finds the key case-insensitively and removes that entry;
* {{removeAll(c)}} removes each element of {{c}}, so the result does not depend
on the sizes;
* {{retainAll(c)}} keeps a key when {{c}} holds it ignoring case;
* {{clear}}, and an iterator whose {{remove}} works ({{removeIf}} uses it).
This makes {{removeAll}} and {{retainAll}} case-insensitive whatever the sizes,
which is more consistent than 4.20 rather than identical to it.
{{CopyOnWriteHeadersMap}} needs no change. {{entrySet().remove}} already finds
the key case-insensitively, and {{values()}} compares values only.
Tests: in {{CaseInsensitiveMapTest}}
({{core/camel-core/src/test/java/org/apache/camel/util/}}):
{{keySet().remove("x-trace")}} removes {{X-Trace}}; {{removeAll}} with a
smaller and with a larger collection; {{retainAll}}; the iterator's {{remove}}
and {{removeIf}}. In {{DefaultMessageHeaderTest}}, one case through
{{getHeaders().keySet()}} of a copied message covers the copy-on-write wrapper.
Duplicate check (2026-09-27): JIRA {{text ~ "CaseInsensitiveMap"}} found
CAMEL-23691, CAMEL-23693, CAMEL-23686 and older issues; {{text ~ "keySet" AND
text ~ "case"}}, and {{text ~ keySet}} since 2026-06-01 (CAMEL-24241,
CAMEL-24350, CAMEL-23971, CAMEL-23681, CAMEL-24622), found nothing related.
GitHub PR searches {{CaseInsensitiveMap}} (#23766, #23779, #25768, #26217),
{{keySet case insensitive}} and {{headers keySet}}: nothing related. Not
reported.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)