Skip to content

Commit 4b732d6

Browse files
committed
Deprecate HttpHeaders.writableHttpHeaders
Prior to this commit, gh-21783 introduced `ReadOnlyHttpHeaders` to avoid parsing media types multiple times during the lifetime of an HTTP exchange: such values are cached and the headers map is made read-only. This also added a new `HttpHeaders.writableHttpHeaders` method to unwrap the read-only variant when needed. It turns out this method sends the wrong signal to the community because: * the underlying map might be unmodifiable even if this is not an instance of ReadOnlyHttpHeaders * developers were assuming that modifying the collection that backs the read-only instance would work around the cached values for Content-Type and Accept headers This commit adds more documentation to highlight the desired behavior for cached values by the read-only variant, and deprecates the `writableHttpHeaders` method as `ReadOnlyHttpHeaders` is package private and we should not surface that concept anyway. Instead, this commit unwraps the read-only variant if needed when a new HttpHeaders instance is created. Closes gh-32116
1 parent 670fc9b commit 4b732d6

File tree

5 files changed

+197
-169
lines changed

5 files changed

+197
-169
lines changed

Diff for: spring-web/src/main/java/org/springframework/http/HttpHeaders.java

+15-7
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,15 @@ public HttpHeaders() {
441441
*/
442442
public HttpHeaders(MultiValueMap<String, String> headers) {
443443
Assert.notNull(headers, "MultiValueMap must not be null");
444-
this.headers = headers;
444+
if (headers == EMPTY) {
445+
this.headers = CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH));
446+
}
447+
else if (headers instanceof ReadOnlyHttpHeaders readOnlyHttpHeaders) {
448+
this.headers = readOnlyHttpHeaders.headers;
449+
}
450+
else {
451+
this.headers = headers;
452+
}
445453
}
446454

447455

@@ -1869,7 +1877,7 @@ public static HttpHeaders readOnlyHttpHeaders(MultiValueMap<String, String> head
18691877
* Apply a read-only {@code HttpHeaders} wrapper around the given headers, if necessary.
18701878
* <p>Also caches the parsed representations of the "Accept" and "Content-Type" headers.
18711879
* @param headers the headers to expose
1872-
* @return a read-only variant of the headers, or the original headers as-is
1880+
* @return a read-only variant of the headers, or the original headers as-is if already read-only
18731881
*/
18741882
public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) {
18751883
Assert.notNull(headers, "HttpHeaders must not be null");
@@ -1879,16 +1887,16 @@ public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) {
18791887
/**
18801888
* Remove any read-only wrapper that may have been previously applied around
18811889
* the given headers via {@link #readOnlyHttpHeaders(HttpHeaders)}.
1890+
* <p>Once the writable instance is mutated, the read-only instance is likely
1891+
* to be out of sync and should be discarded.
18821892
* @param headers the headers to expose
18831893
* @return a writable variant of the headers, or the original headers as-is
18841894
* @since 5.1.1
1895+
* @deprecated as of 6.2 in favor of {@link #HttpHeaders(MultiValueMap)}.
18851896
*/
1897+
@Deprecated(since = "6.2", forRemoval = true)
18861898
public static HttpHeaders writableHttpHeaders(HttpHeaders headers) {
1887-
Assert.notNull(headers, "HttpHeaders must not be null");
1888-
if (headers == EMPTY) {
1889-
return new HttpHeaders();
1890-
}
1891-
return (headers instanceof ReadOnlyHttpHeaders ? new HttpHeaders(headers.headers) : headers);
1899+
return new HttpHeaders(headers);
18921900
}
18931901

18941902
/**

Diff for: spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,6 +31,8 @@
3131

3232
/**
3333
* {@code HttpHeaders} object that can only be read, not written to.
34+
* <p>This caches the parsed representations of the "Accept" and "Content-Type" headers
35+
* and will get out of sync with the backing map it is mutated at runtime.
3436
*
3537
* @author Brian Clozel
3638
* @author Sam Brannen

Diff for: spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public DefaultServerHttpRequestBuilder(ServerHttpRequest original) {
7070
Assert.notNull(original, "ServerHttpRequest is required");
7171

7272
this.uri = original.getURI();
73-
this.headers = HttpHeaders.writableHttpHeaders(original.getHeaders());
73+
this.headers = new HttpHeaders(original.getHeaders());
7474
this.httpMethod = original.getMethod();
7575
this.contextPath = original.getPath().contextPath().value();
7676
this.remoteAddress = original.getRemoteAddress();

0 commit comments

Comments
 (0)