Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate HttpHeaders.writableHttpHeaders #32116

Closed
m42cel opened this issue Jan 25, 2024 · 5 comments
Closed

Deprecate HttpHeaders.writableHttpHeaders #32116

m42cel opened this issue Jan 25, 2024 · 5 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Milestone

Comments

@m42cel
Copy link

m42cel commented Jan 25, 2024

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));
}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jan 25, 2024
@snicoll
Copy link
Member

snicoll commented Jan 26, 2024

The Javadoc of readOnlyHttpHeaders states:

Apply a read-only HttpHeaders wrapper around the given headers, if necessary. Also caches the parsed representations of the "Accept" and "Content-Type" headers.

This looks like the expected behavior to me. I am having trouble understanding the report. If the headers are read-only, that's specifically to avoid changing them. If you insist in doing that, it's asking for trouble really.

@snicoll snicoll added the status: waiting-for-feedback We need additional information before we can continue label Jan 26, 2024
@m42cel
Copy link
Author

m42cel commented Jan 28, 2024

I agree that trying to change read-only objects is asking for trouble, which is a general design-flaw with how HttpHeaders.writableHttpHeaders works, because it doesn't return a modifiable copy but instead allows modifications to the original read-only object.
However, as-is, returning a writable copy would also be problematic. Intercepting HttpRequests and doing some modifications to it before continuing in the processing chain is a valid use-case. But since the HttpRequest only provides a getHeaders() and no setHeaders(), it calls for modifying the object that was returned via getHeaders().

It looks like the ReadOnlyHttpHeaders was introduced to improve performance by caching some often requested properties. I currently could think of two options to solve the described issue:

  1. Instead of putting the two cached headers into the underlying MultiValueMap store them in dedicated fields in the HttpHeaders. As it is now, reading via the getContentType() could directly go to the field. When reading via get(CONTENT_TYPE) there would have to be a check if the key is Content-Type or Accept and then use the fields instead of the map (same for set). This would have to be done in the original HttpHeaders class.
  2. Make HttpHeaders.writableHttpHeaders return a writable-copy. Then there also has to be a way to set HttpHeaders to HttpRequests or to easily create a modifiable copy of the request. Like for example it is available with Spring Reactive's ServerHttpRequest::mutate.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jan 28, 2024
@sbrannen sbrannen changed the title ReadOnlyHttpHeaders issue with cachedContentType if Content-Type is modified via HttpHeaders.writableHttpHeaders() ReadOnlyHttpHeaders issue with cachedContentType if Content-Type is modified via HttpHeaders.writableHttpHeaders() Feb 1, 2024
@sbrannen sbrannen added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Feb 1, 2024
@snicoll snicoll changed the title ReadOnlyHttpHeaders issue with cachedContentType if Content-Type is modified via HttpHeaders.writableHttpHeaders() ReadOnlyHttpHeaders issue with cachedContentType if Content-Type is modified via HttpHeaders.writableHttpHeaders() Feb 2, 2024
@bclozel bclozel self-assigned this Feb 13, 2024
@bclozel
Copy link
Member

bclozel commented Feb 13, 2024

Thanks for raising this.
Indeed, ReadOnlyHttpHeaders operate under the assumption that the underlying collection of headers is immutable in order to cache expensive values. It's not meant to work while another process is mutating those values - the behavior you're demonstrating is expected. Adapting to this use case would partially undo this performance improvement as we'd need to perform map lookups and value comparison every time the content type is asked for, which can be multiple times during request processing.

If you'd like to mutate the HttpServletRequest, I think wrapping it is the usual solution for that use case. In Spring web frameworks (and our own request types), we often provide mutators for this very purpose.

We'll use this issue to deprecate HttpHeaders.writableHttpHeaders and prepare for its removal, as it was mainly for internal use in the first place. I guess we can live with some duplication in our codebase.

@bclozel bclozel added type: task A general task and removed status: waiting-for-triage An issue we've not yet triaged or decided on status: feedback-provided Feedback has been provided labels Feb 13, 2024
@bclozel bclozel added this to the 6.2.x milestone Feb 13, 2024
@bclozel bclozel changed the title ReadOnlyHttpHeaders issue with cachedContentType if Content-Type is modified via HttpHeaders.writableHttpHeaders() Deprecate HttpHeaders.writableHttpHeaders Feb 13, 2024
@snicoll snicoll added type: enhancement A general enhancement and removed type: task A general task labels Feb 13, 2024
@m42cel
Copy link
Author

m42cel commented Feb 19, 2024

I hope the people of the future won't lynch me after the removal breaks their code base and they find this issue 😰 (if someone from the future is reading this, feel free to leave a "👋").

Jokes aside, I feel like there could be a little more support for handling, mutating and testing ReadOnlyHttpHeaders. The update to Spring Boot 3.2 did cost us quite some effort with HttpHeaders being made read-only. A few points I missed when adapting to the ReadOnlyHttpHeaders:

  • There's org.springframework.http.client.support.HttpRequestWrapper but no HttpResponseWrapper
  • There's no copy-constructor of copy method in HttpHeaders which makes creating a writable copy in a wrapper class a bit awkward
  • ReadOnlyHttpHeaders are often only created during runtime, while spring-testing provides a MockHttpInputMessage containing a writable HttpHeaders instance. This makes it harder to unit-test if your code can handle ReadOnlyHttpHeaders, that are usually created by spring-web during runtime
    (I created a ReadOnlyHeaderMockHttpInputMessage for that purpose, which contains ReadOnlyHttpHeaders but allows setting headers for test initialization via separated methods)

Maybe I'm overlooking some mutators but to me it looked like most mutator functionality was only provided for Spring Reactive.

@bclozel
Copy link
Member

bclozel commented Feb 20, 2024

@m42cel that's interesting feedback. We'll have a look and see if we need to create new issues to help with that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

5 participants