HttpHeaders are no longer a MultiValueMap

This change removes the `MultiValueMap` nature of `HttpHeaders`, since
it inherits APIs that do not align well with underlying server
implementations. Notably, methods that allows to iterate over the whole
collection of headers are susceptible to artificially introduced
duplicates when multiple casings are used for a given header, depending
on the underlying implementation.

This change includes a dedicated key set implementation to support
iterator-based removal, and either keeps map method implementations that
are relevant or introduces header-focused methods that have a similar
responsibility (like `hasHeaderValues(String, List)` and
`containsHeaderValue(String, String)`).

In order to nudge users away from using an HttpHeaders as a Map, the
`asSingleValueMap` view is deprecated. In order to offer an escape
hatch to users that do make use of the `MultiValueMap` API, a similar
`asMultiValueMap` view is introduced but is immediately marked as
deprecated.

This change also adds map-like but header-focused assertions to
`HttpHeadersAssert`, since it cannot extend `AbstractMapAssert` anymore.

Closes gh-33913
This commit is contained in:
Simon Baslé
2024-12-02 14:55:27 +01:00
parent 1e0ef99b0c
commit 0c6f5d7d29
100 changed files with 1116 additions and 508 deletions

View File

@@ -17,7 +17,6 @@
package org.springframework.web.socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -244,30 +243,30 @@ public class WebSocketHttpHeaders extends HttpHeaders {
}
@Override
public boolean containsKey(Object key) {
return this.headers.containsKey(key);
public boolean containsHeader(String key) {
return this.headers.containsHeader(key);
}
@Override
public boolean containsValue(Object value) {
return this.headers.containsValue(value);
public @Nullable List<String> get(String headerName) {
return this.headers.get(headerName);
}
@Override
public @Nullable List<String> get(Object key) {
return this.headers.get(key);
}
@Override
public List<String> put(String key, List<String> value) {
public @Nullable List<String> put(String key, List<String> value) {
return this.headers.put(key, value);
}
@Override
public List<String> remove(Object key) {
public @Nullable List<String> remove(String key) {
return this.headers.remove(key);
}
@Override
public void putAll(HttpHeaders headers) {
this.headers.putAll(headers);
}
@Override
public void putAll(Map<? extends String, ? extends List<String>> m) {
this.headers.putAll(m);
@@ -279,18 +278,13 @@ public class WebSocketHttpHeaders extends HttpHeaders {
}
@Override
public Set<String> keySet() {
return this.headers.keySet();
public Set<String> headerNames() {
return this.headers.headerNames();
}
@Override
public Collection<List<String>> values() {
return this.headers.values();
}
@Override
public Set<Entry<String, List<String>>> entrySet() {
return this.headers.entrySet();
public Set<Map.Entry<String, List<String>>> headerSet() {
return this.headers.headerSet();
}
@Override
@@ -299,8 +293,8 @@ public class WebSocketHttpHeaders extends HttpHeaders {
}
@Override
public List<String> putIfAbsent(String key, List<String> value) {
return this.headers.putIfAbsent(key, value);
public @Nullable List<String> putIfAbsent(String headerName, List<String> headerValues) {
return this.headers.putIfAbsent(headerName, headerValues);
}

View File

@@ -216,7 +216,7 @@ public class StandardWebSocketClient extends AbstractWebSocketClient {
@Override
public void beforeRequest(Map<String, List<String>> requestHeaders) {
requestHeaders.putAll(this.headers);
this.headers.forEach(requestHeaders::put);
if (logger.isTraceEnabled()) {
logger.trace("Handshake request headers: " + requestHeaders);
}

View File

@@ -100,7 +100,7 @@ class StandardWebSocketClientTests {
WebSocketSession session = this.wsClient.execute(this.wsHandler, this.headers, uri).get();
assertThat(session.getHandshakeHeaders()).hasSize(1);
assertThat(session.getHandshakeHeaders().size()).isOne();
assertThat(session.getHandshakeHeaders().getFirst("foo")).isEqualTo("bar");
}

View File

@@ -108,12 +108,12 @@ class SockJsClientTests {
this.sockJsClient.execute(handler, headers, URI.create(URL)).whenComplete(this.connectCallback);
HttpHeaders httpHeaders = headersCaptor.getValue();
assertThat(httpHeaders).hasSize(2);
assertThat(httpHeaders.size()).isEqualTo(2);
assertThat(httpHeaders.getFirst("foo")).isEqualTo("bar");
assertThat(httpHeaders.getFirst("auth")).isEqualTo("123");
httpHeaders = this.xhrTransport.getRequest().getHttpRequestHeaders();
assertThat(httpHeaders).hasSize(2);
assertThat(httpHeaders.size()).isEqualTo(2);
assertThat(httpHeaders.getFirst("foo")).isEqualTo("bar");
assertThat(httpHeaders.getFirst("auth")).isEqualTo("123");
}
@@ -129,9 +129,9 @@ class SockJsClientTests {
this.sockJsClient.setHttpHeaderNames("auth");
this.sockJsClient.execute(handler, headers, URI.create(URL)).whenComplete(this.connectCallback);
assertThat(headersCaptor.getValue()).hasSize(1);
assertThat(headersCaptor.getValue().size()).isEqualTo(1);
assertThat(headersCaptor.getValue().getFirst("auth")).isEqualTo("123");
assertThat(this.xhrTransport.getRequest().getHttpRequestHeaders()).hasSize(1);
assertThat(this.xhrTransport.getRequest().getHttpRequestHeaders().size()).isEqualTo(1);
assertThat(this.xhrTransport.getRequest().getHttpRequestHeaders().getFirst("auth")).isEqualTo("123");
}

View File

@@ -69,7 +69,7 @@ class XhrTransportTests {
transport.sendMessageResponseToReturn = new ResponseEntity<>(HttpStatus.NO_CONTENT);
URI url = URI.create("https://example.com");
transport.executeSendRequest(url, requestHeaders, new TextMessage("payload"));
assertThat(transport.actualSendRequestHeaders).hasSize(2);
assertThat(transport.actualSendRequestHeaders.size()).isEqualTo(2);
assertThat(transport.actualSendRequestHeaders.getFirst("foo")).isEqualTo("bar");
assertThat(transport.actualSendRequestHeaders.getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
}
@@ -105,7 +105,7 @@ class XhrTransportTests {
verify(request).getHttpRequestHeaders();
verifyNoMoreInteractions(request);
assertThat(transport.actualHandshakeHeaders).hasSize(1);
assertThat(transport.actualHandshakeHeaders.size()).isOne();
assertThat(transport.actualHandshakeHeaders.getOrigin()).isEqualTo("foo");
assertThat(transport.actualSession.isDisconnected()).isFalse();