Hello Spring maintainers,

I've found a bug in the NettyHeadersAdapter that causes it to duplicate header values when the string case is mixed up between entries.

This arises because the adapter iterates through the underlying entries using a case-insensitive method to get the values.

DefaultHttpHeaders headers;
for (var n : headers.names()) {       <-- case sensitive
  for (var v : headers.getAll(n)) {   <-- case insensitive
    System.out.println(n + "=" + v);
  }
}

I've reproduced this on 6.2.0 Reproducer:

var headers = new DefaultHttpHeaders();
var adapter = new Netty5HeadersAdapter(headers);  // Also with Netty4
adapter.add("foo-bar", "one");
adapter.add("Foo-Bar", "two");
adapter.add("fOO-bar", "three");
for (var e : adapter.entrySet()) {
   System.out.println(e.getKey() + "=" + e.getValue());
}

/*
Output:
foo-bar=[one, two, three]
Foo-Bar=[one, two, three]
fOO-bar=[one, two, three]
*/

Comment From: bclozel

@mtheos have you seen this part of the javadoc?

https://github.com/spring-projects/spring-framework/blob/082a8cfae3dc7a0b8befa3f4c310cf68f4de41ab/spring-web/src/main/java/org/springframework/http/HttpHeaders.java#L68

Comment From: mtheos

Hi @bclozel, I haven't; thanks for pointing it out. I can't say I'm a big fan of that API 😓 but if you know about the behaviour and don't plan to change it, I can work around it.

Comment From: bclozel

We're not big fans of this either and we plan to revisit this arrangement altogether in #33913.

In the past, this wasn't an issue as we were copying all "native" (as in the underlying server implementation) headers into our own multivalue map, and then back. We found out that this was one of the main source of framework overhead and adapting to native headers improved throughput and allocation performance significantly.

Most server implementations do not model headers as a multivalue map, but rather as a collection of name/values pairs. They're only considering the case insensitive behavior for a few dedicated methods. Iterating over headers does not take this into account.

So we're stuck between making the performance way worse for all, or reconsidering this aspect in the future. For now, following the advice in the javadoc is your best bet and we'll figure out a way to improve the API in the future.