The current NettyHeaders
supports case-sensitive storage of entry values when using the add
method. This causes the NettyHeadersAdapter
to accumulate the values for the corresponding key when retrieving headers(see unit test).
Add a unit test testHeadersOutput to verify the final output of the headers.
Comment From: qnnn
addAll has the same issue, and it has also been addressed.
Comment From: simonbasle
hey @qnnn thanks for the PR. Can you elaborate on the use case that lead you to come up with this change? I'm guessing this is a matter of enumerating the headers or representing them as a whole?
Comment From: qnnn
hey @qnnn thanks for the PR. Can you elaborate on the use case that lead you to come up with this change? I'm guessing this is a matter of enumerating the headers or representing them as a whole?
@simonbasle I discovered this problem while using the AddRequestHeader GatewayFilter in Spring Cloud Gateway. Through network packet capture, it can be seen that the HTTP requests sent by the Gateway aggregate the values of case-sensitive keys in the HTTP headers. The expected request header should be testheader=first, second, but the actual result is testheader=first, second, first, second (the same as the output in the unit test).
Comment From: simonbasle
Thanks for your feedback. We're exploring what we can do to improve enumeration/iteration of headers through the framework's MultiValueMap
adapters, which seems to be the root of the issue. Basically, iterating over the entrySet or dumping a string representation in logs is susceptible to display fabricated duplicates for header names that have been added with multiple casing.
While all native implementations of http headers enforce case-insensitive access (as mandated by the spec), some still store headers as name-value arrays / lists where each entry's might have differences in casing.
This leads to apparent inconsistencies for "whole collection" methods when we try to offer a MultiValueMap
view over these implementations, as they do not align well with that representation. Most notably:
- size()
- entrySet()
(set of Entry<String, List<String>>>
) which is used by HttpHeaders#formatHeaders
- keySet()
- values()
Can you confirm whether or not the duplication of values for a given header name happens on the wire however? (eg. using Wireshark)
Comment From: qnnn
@simonbasle Thanks for your patience in explaining this. Yes, I can confirm that the duplication of values for a given header name happens on the wire (confirmed via Wireshark).
It seems that some Spring Cloud Gateway's header filters
are causing this issue when they transform the NettyHeaderAdapter
into MultiValueMapAdapter<>(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH))
(such as in org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilter#filter
).
I feel that MultiValueMapAdapter<>(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH))
is a standards-compliant header implementation, and when other header adapters are converted to it, they should produce the same result (case-insensitive).
Comment From: sdeleuze
@qnnn Is this use case of headers filtering/reconstruction in org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilter
is the only concrete bug you face or are there other ones (if yes, which ones)?
Comment From: qnnn
@sdeleuze Yes, I have verified that there are others as well:
- org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter
- org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter
Any implementation that iterates through the Netty Headers Adapter
's entrySet
and then uses addAll
may have this issue. Such as(I haven’t verified this yet; it's just a guess):
- org.springframework.cloud.gateway.filter.WebsocketRoutingFilter
- org.springframework.cloud.gateway.filter.headers.TransferEncodingNormalizationHeadersFilter
- org.springframework.cloud.gateway.filter.factory.cache.CachedResponse$Builder#headers
Comment From: sdeleuze
Thanks, that helps. We are discussing the best strategy to fix those bugs while keeping a great performance/efficiency. We will get back to you once we have made a decision.
Comment From: qnnn
Thanks, that helps. We are discussing the best strategy to fix those bugs while keeping a great performance/efficiency. We will get back to you once we have made a decision.
@sdeleuze It's ok, I understand.
Perhaps maintaining a caseInsensitiveKeyMap
to store case-insensitive keys might help with performance—(please disregard if it’s not useful.)
When operating on NettyHeaders
, the following map could be updated simultaneously:
Map<String, String> map = new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH);
String headerKey = map.compute("TestHeader", (caseInsensitiveKey, headerKey) -> {
if (!StringUtils.hasText(headerKey)){
return caseInsensitiveKey;
}
return headerKey;
});
Something similar:
Comment From: simonbasle
@qnnn we continue evaluating the possible fixes and their impact. I will probably close this PR in favor of creating a new issue tracking this, in which case I'll ping you in there.
Comment From: qnnn
Correct me if I'm wrong, but the headers case variants are populated by Netty itself, i.e. it comes from the wire. In that case you are addressing the wrong issue.
The change and tests that you proposed impact the intermediate
Netty4HeadersAdapter
(adapting NettyDefaultHttpHeaders
toMultiValueMap<String, String>
)... but if you add the test's headers toDefaultHttpHeader
instance itself, then construct aNetty4HeadersAdapter
out of it, tests fail again despite your changes:```java @Test void prepopulatedNativeNetty4() { DefaultHttpHeaders native = new DefaultHttpHeaders(); native.add("TestHeader", "first"); native.add("TestHEADER", "second"); native.add("SecondHeader", "value"); native.add("TestHeader", "third");
MultiValueMap<String, String> adapter = new Netty4HeadersAdapter(native); //copying native headers into a new HttpHeaders HttpHeaders headers = new HttpHeaders(); for (Map.Entry<String, List<String>> entry : adapter.entrySet()) { headers.addAll(entry.getKey(), entry.getValue()); } assertThat(headers.get("TestHeader")).as("TestHeader") .containsExactly("first", "second", "third");
} ```
@simonbasle Indeed, perhaps this can be reset during the Adapter initialization? In my understanding, Netty itself can function normally.
Comment From: simonbasle
Superseded by gh-33823