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
This commit is contained in:
parent
670fc9bb4e
commit
4b732d62c2
|
|
@ -441,8 +441,16 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
|
||||||
*/
|
*/
|
||||||
public HttpHeaders(MultiValueMap<String, String> headers) {
|
public HttpHeaders(MultiValueMap<String, String> headers) {
|
||||||
Assert.notNull(headers, "MultiValueMap must not be null");
|
Assert.notNull(headers, "MultiValueMap must not be null");
|
||||||
|
if (headers == EMPTY) {
|
||||||
|
this.headers = CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH));
|
||||||
|
}
|
||||||
|
else if (headers instanceof ReadOnlyHttpHeaders readOnlyHttpHeaders) {
|
||||||
|
this.headers = readOnlyHttpHeaders.headers;
|
||||||
|
}
|
||||||
|
else {
|
||||||
this.headers = headers;
|
this.headers = headers;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1869,7 +1877,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
|
||||||
* Apply a read-only {@code HttpHeaders} wrapper around the given headers, if necessary.
|
* Apply a read-only {@code HttpHeaders} wrapper around the given headers, if necessary.
|
||||||
* <p>Also caches the parsed representations of the "Accept" and "Content-Type" headers.
|
* <p>Also caches the parsed representations of the "Accept" and "Content-Type" headers.
|
||||||
* @param headers the headers to expose
|
* @param headers the headers to expose
|
||||||
* @return a read-only variant of the headers, or the original headers as-is
|
* @return a read-only variant of the headers, or the original headers as-is if already read-only
|
||||||
*/
|
*/
|
||||||
public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) {
|
public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) {
|
||||||
Assert.notNull(headers, "HttpHeaders must not be null");
|
Assert.notNull(headers, "HttpHeaders must not be null");
|
||||||
|
|
@ -1879,16 +1887,16 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
|
||||||
/**
|
/**
|
||||||
* Remove any read-only wrapper that may have been previously applied around
|
* Remove any read-only wrapper that may have been previously applied around
|
||||||
* the given headers via {@link #readOnlyHttpHeaders(HttpHeaders)}.
|
* the given headers via {@link #readOnlyHttpHeaders(HttpHeaders)}.
|
||||||
|
* <p>Once the writable instance is mutated, the read-only instance is likely
|
||||||
|
* to be out of sync and should be discarded.
|
||||||
* @param headers the headers to expose
|
* @param headers the headers to expose
|
||||||
* @return a writable variant of the headers, or the original headers as-is
|
* @return a writable variant of the headers, or the original headers as-is
|
||||||
* @since 5.1.1
|
* @since 5.1.1
|
||||||
|
* @deprecated as of 6.2 in favor of {@link #HttpHeaders(MultiValueMap)}.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(since = "6.2", forRemoval = true)
|
||||||
public static HttpHeaders writableHttpHeaders(HttpHeaders headers) {
|
public static HttpHeaders writableHttpHeaders(HttpHeaders headers) {
|
||||||
Assert.notNull(headers, "HttpHeaders must not be null");
|
return new HttpHeaders(headers);
|
||||||
if (headers == EMPTY) {
|
|
||||||
return new HttpHeaders();
|
|
||||||
}
|
|
||||||
return (headers instanceof ReadOnlyHttpHeaders ? new HttpHeaders(headers.headers) : headers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2023 the original author or authors.
|
* Copyright 2002-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -31,6 +31,8 @@ import org.springframework.util.MultiValueMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@code HttpHeaders} object that can only be read, not written to.
|
* {@code HttpHeaders} object that can only be read, not written to.
|
||||||
|
* <p>This caches the parsed representations of the "Accept" and "Content-Type" headers
|
||||||
|
* and will get out of sync with the backing map it is mutated at runtime.
|
||||||
*
|
*
|
||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
* @author Sam Brannen
|
* @author Sam Brannen
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder {
|
||||||
Assert.notNull(original, "ServerHttpRequest is required");
|
Assert.notNull(original, "ServerHttpRequest is required");
|
||||||
|
|
||||||
this.uri = original.getURI();
|
this.uri = original.getURI();
|
||||||
this.headers = HttpHeaders.writableHttpHeaders(original.getHeaders());
|
this.headers = new HttpHeaders(original.getHeaders());
|
||||||
this.httpMethod = original.getMethod();
|
this.httpMethod = original.getMethod();
|
||||||
this.contextPath = original.getPath().contextPath().value();
|
this.contextPath = original.getPath().contextPath().value();
|
||||||
this.remoteAddress = original.getRemoteAddress();
|
this.remoteAddress = original.getRemoteAddress();
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import java.util.Map.Entry;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import static java.util.stream.Collectors.toList;
|
import static java.util.stream.Collectors.toList;
|
||||||
|
|
@ -54,8 +55,19 @@ import static org.assertj.core.api.Assertions.entry;
|
||||||
*/
|
*/
|
||||||
class HttpHeadersTests {
|
class HttpHeadersTests {
|
||||||
|
|
||||||
private final HttpHeaders headers = new HttpHeaders();
|
final HttpHeaders headers = new HttpHeaders();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constructorUnwrapsReadonly() {
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
HttpHeaders readOnly = HttpHeaders.readOnlyHttpHeaders(headers);
|
||||||
|
assertThat(readOnly.getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||||
|
HttpHeaders writable = new HttpHeaders(readOnly);
|
||||||
|
writable.setContentType(MediaType.TEXT_PLAIN);
|
||||||
|
// content-type value is cached by ReadOnlyHttpHeaders
|
||||||
|
assertThat(readOnly.getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||||
|
assertThat(writable.getContentType()).isEqualTo(MediaType.TEXT_PLAIN);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getOrEmpty() {
|
void getOrEmpty() {
|
||||||
|
|
@ -578,6 +590,10 @@ class HttpHeadersTests {
|
||||||
assertThat(authorization).isEqualTo("Bearer foo");
|
assertThat(authorization).isEqualTo("Bearer foo");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class MapEntriesTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void keySetOperations() {
|
void keySetOperations() {
|
||||||
headers.add("Alpha", "apple");
|
headers.add("Alpha", "apple");
|
||||||
|
|
@ -757,3 +773,5 @@ class HttpHeadersTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder {
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
private HttpHeaders getHeaders() {
|
private HttpHeaders getHeaders() {
|
||||||
if (this.headers == null) {
|
if (this.headers == null) {
|
||||||
this.headers = HttpHeaders.writableHttpHeaders(this.originalResponse.headers().asHttpHeaders());
|
this.headers = new HttpHeaders(this.originalResponse.headers().asHttpHeaders());
|
||||||
}
|
}
|
||||||
return this.headers;
|
return this.headers;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue