Fix HttpHeaders and WebSocketHttpHeaders interop issues

Since HttpHeaders no longer implements MultiValueMap (see gh-33913),
a few interoperability issues have arisen between HttpHeaders and
WebSocketHttpHeaders.

To address those issues, this commit:

- Revises addAll(HttpHeaders), putAll(HttpHeaders), and putAll(Map) in
  HttpHeaders so that they no longer operate on the HttpHeaders.headers
  field.

- Overrides addAll(String, List), asSingleValueMap(), and
  asMultiValueMap() in WebSocketHttpHeaders.

- Deletes putAll(HttpHeaders), putAll(Map), and forEach(BiConsumer) in
  WebSocketHttpHeaders, since they do not need to be overridden.

This commit also removes unnecessarily overridden Javadoc in
WebSocketHttpHeaders and revises the implementation of several methods
in HttpHeaders so that they delegate to key methods such as get()
instead of directly accessing the HttpHeaders.headers field.

See gh-33913
Closes gh-35792
This commit is contained in:
Sam Brannen
2025-11-11 18:29:29 +01:00
parent a3e8a173a8
commit 4593f877dd
4 changed files with 181 additions and 83 deletions
@@ -16,41 +16,122 @@
package org.springframework.web.socket.handler;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.web.socket.WebSocketExtension;
import org.springframework.http.HttpHeaders;
import org.springframework.web.socket.WebSocketHttpHeaders;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
/**
* Tests for {@link WebSocketHttpHeaders}.
*
* @author Rossen Stoyanchev
* @author Sam Brannen
*/
class WebSocketHttpHeadersTests {
private WebSocketHttpHeaders headers;
private WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
@BeforeEach
void setUp() {
headers = new WebSocketHttpHeaders();
}
@Test
void parseWebSocketExtensions() {
List<String> extensions = new ArrayList<>();
extensions.add("x-foo-extension, x-bar-extension");
extensions.add("x-test-extension");
var extensions = List.of("x-foo-extension, x-bar-extension", "x-test-extension");
this.headers.put(WebSocketHttpHeaders.SEC_WEBSOCKET_EXTENSIONS, extensions);
List<WebSocketExtension> parsedExtensions = this.headers.getSecWebSocketExtensions();
var parsedExtensions = this.headers.getSecWebSocketExtensions();
assertThat(parsedExtensions).hasSize(3);
}
@Test // gh-35792
void addAllViaWebSocketHttpHeadersApi() {
headers.add("green", "grape");
var otherHeaders = new HttpHeaders();
otherHeaders.add("yellow", "banana");
otherHeaders.add("red", "apple");
headers.addAll(otherHeaders);
assertThat(headers.toSingleValueMap()).containsOnly(
entry("green", "grape"),
entry("yellow", "banana"),
entry("red", "apple")
);
}
@Test // gh-35792
void addAllViaHttpHeadersApi() {
headers.add("yellow", "banana");
headers.add("red", "apple");
var otherHeaders = new HttpHeaders();
otherHeaders.add("green", "grape");
otherHeaders.addAll(headers);
assertThat(otherHeaders.toSingleValueMap()).containsOnly(
entry("green", "grape"),
entry("yellow", "banana"),
entry("red", "apple")
);
}
@Test // gh-35792
void putAllFromHttpHeadersViaWebSocketHttpHeadersApi() {
var otherHeaders = new HttpHeaders();
otherHeaders.add("yellow", "banana");
otherHeaders.add("red", "apple");
headers.putAll(otherHeaders);
assertThat(headers.toSingleValueMap()).containsOnly(
entry("yellow", "banana"),
entry("red", "apple")
);
}
@Test // gh-35792
void putAllFromHttpHeadersViaHttpHeadersApi() {
headers.add("yellow", "banana");
headers.add("red", "apple");
var otherHeaders = new HttpHeaders();
otherHeaders.putAll(headers);
assertThat(otherHeaders.toSingleValueMap()).containsOnly(
entry("yellow", "banana"),
entry("red", "apple")
);
}
@Test // gh-35792
void putAllFromMap() {
headers.putAll(Map.of("yellow", List.of("banana"), "red", List.of("apple")));
assertThat(headers.toSingleValueMap()).containsOnly(
entry("yellow", "banana"),
entry("red", "apple")
);
}
@Test // gh-35792
void setAllFromMap() {
headers.add("yellow", "lemon");
assertThat(headers.toSingleValueMap()).containsOnly(
entry("yellow", "lemon")
);
headers.setAll(Map.of("yellow", "banana", "red", "apple"));
assertThat(headers.toSingleValueMap()).containsOnly(
entry("yellow", "banana"), // not lemon
entry("red", "apple")
);
}
}
@@ -29,10 +29,12 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketHttpHeaders;
import org.springframework.web.socket.WebSocketSession;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -42,19 +44,38 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
* Tests for {@link AbstractXhrTransport}.
*
* @author Rossen Stoyanchev
* @author Sam Brannen
*/
class XhrTransportTests {
private final TestXhrTransport transport = new TestXhrTransport();
@Test
void infoResponse() {
TestXhrTransport transport = new TestXhrTransport();
transport.infoResponseToReturn = new ResponseEntity<>("body", HttpStatus.OK);
assertThat(transport.executeInfoRequest(URI.create("https://example.com/info"), null)).isEqualTo("body");
}
@Test // gh-35792
void infoResponseWithWebSocketHttpHeaders() {
transport.infoResponseToReturn = new ResponseEntity<>("body", HttpStatus.OK);
var headers = new WebSocketHttpHeaders();
headers.setSecWebSocketAccept("enigma");
headers.add("foo", "bar");
transport.executeInfoRequest(URI.create("https://example.com/info"), headers);
assertThat(transport.actualInfoHeaders).isNotNull();
assertThat(transport.actualInfoHeaders.toSingleValueMap()).containsExactly(
entry(WebSocketHttpHeaders.SEC_WEBSOCKET_ACCEPT, "enigma"),
entry("foo", "bar")
);
}
@Test
void infoResponseError() {
TestXhrTransport transport = new TestXhrTransport();
transport.infoResponseToReturn = new ResponseEntity<>("body", HttpStatus.BAD_REQUEST);
assertThatExceptionOfType(HttpServerErrorException.class).isThrownBy(() ->
transport.executeInfoRequest(URI.create("https://example.com/info"), null));
@@ -65,7 +86,6 @@ class XhrTransportTests {
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.set("foo", "bar");
requestHeaders.setContentType(MediaType.APPLICATION_JSON);
TestXhrTransport transport = new TestXhrTransport();
transport.sendMessageResponseToReturn = new ResponseEntity<>(HttpStatus.NO_CONTENT);
URI url = URI.create("https://example.com");
transport.executeSendRequest(url, requestHeaders, new TextMessage("payload"));
@@ -76,7 +96,6 @@ class XhrTransportTests {
@Test
void sendMessageError() {
TestXhrTransport transport = new TestXhrTransport();
transport.sendMessageResponseToReturn = new ResponseEntity<>(HttpStatus.BAD_REQUEST);
URI url = URI.create("https://example.com");
assertThatExceptionOfType(HttpServerErrorException.class).isThrownBy(() ->
@@ -93,7 +112,6 @@ class XhrTransportTests {
given(request.getHandshakeHeaders()).willReturn(handshakeHeaders);
given(request.getHttpRequestHeaders()).willReturn(new HttpHeaders());
TestXhrTransport transport = new TestXhrTransport();
WebSocketHandler handler = mock();
transport.connectAsync(request, handler);
@@ -124,10 +142,13 @@ class XhrTransportTests {
private HttpHeaders actualHandshakeHeaders;
private HttpHeaders actualInfoHeaders;
private XhrClientSockJsSession actualSession;
@Override
protected ResponseEntity<String> executeInfoRequestInternal(URI infoUrl, HttpHeaders headers) {
this.actualInfoHeaders = headers;
return this.infoResponseToReturn;
}