Description
Affects: 5.1 / 6.1
What
This problem arises from the fact the Spring allows to remove the ReadOnlyHttpHeaders
wrapper without creating a modifiable copy but instead modifies the underlying MultiValueMap
and thereby allows modifying an ReadOnlyHttpHeaders
object.
When using ReadOnlyHttpHeaders::getContentType
it will cache the returned Content-Type, which was introduced in Improve WebFlux performance for header management [SPR-17250].
However, if the Content-Type of the original HttpHeaders
object is changed, the cache is not invalidated and a successive call to readOnlyHttpHeaders.getContentType()
will return the outdated Content-Type, while readOnlyHttpHeaders.getFirst(HttpHeaders.CONTENT_TYPE)
will return the new value.
Background
With the update from Spring Boot 3.1 to 3.2 many HttpHeaders
created by Spring Web seem to be changed to ReadOnlyHttpHeaders
. This broke many parts of our project, since we often intercept requests to do some modifications (like removing specific headers before forwarding the request to an internal host). Since it's not possible to set a new HttpHeaders
object to a HttpRequest
or HttpServletRequest
we now use HttpHeaders::writableHttpHeaders
method to do modifications of the original headers.
This however does not work for the Content-Type once it is cached in the HttpServletRequest
's ReadOnlyHttpHeaders
.
One solution would be to completely wrap the original HttpRequest
before passing it on in the interceptor chain (which I think is the recommended approach) but that doesn't change the fact that the HttpHeaders::writableHttpHeaders
and cachedContentType
cause some inconsistency.
Example
@Test
void testReadOnlyHeaders() {
HttpHeaders originalHeaders = new HttpHeaders();
originalHeaders.setContentType(MediaType.APPLICATION_XML);
// only a ready-only wrapper around the original HttpHeaders
HttpHeaders readOnlyHttpHeaders = HttpHeaders.readOnlyHttpHeaders(originalHeaders);
// caches the content type value
assertThat(readOnlyHttpHeaders.getContentType()).isEqualTo(MediaType.APPLICATION_XML);
// creates a writable variant of the ReadOnlyHttpHeaders
HttpHeaders writableHttpHeaders = HttpHeaders.writableHttpHeaders(readOnlyHttpHeaders);
// changes the value in the underlying MultiValueMap but doesn't invalidate the cached value
writableHttpHeaders.setContentType(MediaType.APPLICATION_JSON);
// passes since it doesn't use the cached value
assertThat(writableHttpHeaders.getContentType()).hasToString(writableHttpHeaders.getFirst(HttpHeaders.CONTENT_TYPE));
// fails since the cached value is still application/xml
assertThat(readOnlyHttpHeaders.getContentType()).hasToString(readOnlyHttpHeaders.getFirst(HttpHeaders.CONTENT_TYPE));
}