Skip to content

Deprecate HttpHeaders.writableHttpHeaders #32116

Closed
@m42cel

Description

@m42cel

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));
}

Metadata

Metadata

Assignees

Labels

in: webIssues in web modules (web, webmvc, webflux, websocket)type: enhancementA general enhancement

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions