Skip to content

Commit f62380c

Browse files
committed
Make WebSocketHttpHeaders compatible with HttpHeaders APIs
Prior to this commit (and despite the changes made in commit 4593f87), WebSocketHttpHeaders was not compatible with the HttpHeaders(HttpHeaders) constructor or the copyOf(HttpHeaders) and readOnlyHttpHeaders(HttpHeaders) factory methods. To address that, this commit revises the implementation of WebSocketHttpHeaders so that it only extends HttpHeaders, analogous to ReadOnlyHttpHeaders. In other words, WebSocketHttpHeaders no longer stores or delegates to a local HttpHeaders instance. Closes gh-35792
1 parent 93b72fa commit f62380c

File tree

2 files changed

+118
-145
lines changed

2 files changed

+118
-145
lines changed

spring-websocket/src/main/java/org/springframework/web/socket/WebSocketHttpHeaders.java

Lines changed: 21 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,15 @@
1919
import java.util.ArrayList;
2020
import java.util.Collections;
2121
import java.util.List;
22-
import java.util.Map;
23-
import java.util.Set;
2422

2523
import org.jspecify.annotations.Nullable;
2624

2725
import org.springframework.http.HttpHeaders;
2826
import org.springframework.util.CollectionUtils;
29-
import org.springframework.util.MultiValueMap;
3027

3128
/**
32-
* An {@link org.springframework.http.HttpHeaders} variant that adds support for
33-
* the HTTP headers defined by the WebSocket specification RFC 6455.
29+
* An {@link HttpHeaders} variant that adds support for the HTTP headers defined
30+
* by the WebSocket specification RFC 6455.
3431
*
3532
* @author Rossen Stoyanchev
3633
* @author Sam Brannen
@@ -51,23 +48,32 @@ public class WebSocketHttpHeaders extends HttpHeaders {
5148
private static final long serialVersionUID = -6644521016187828916L;
5249

5350

54-
private final HttpHeaders headers;
55-
56-
5751
/**
58-
* Create a new instance.
52+
* Construct a new, empty {@code WebSocketHttpHeaders} instance.
5953
*/
6054
public WebSocketHttpHeaders() {
61-
this(new HttpHeaders());
55+
super();
6256
}
6357

6458
/**
65-
* Create an instance that wraps the given pre-existing HttpHeaders and also
66-
* propagate all changes to it.
67-
* @param headers the HTTP headers to wrap
59+
* Construct a new {@code WebSocketHttpHeaders} instance backed by the supplied
60+
* {@code HttpHeaders}.
61+
* <p>Changes to the {@code WebSocketHttpHeaders} created by this constructor
62+
* will write through to the supplied {@code HttpHeaders}. If you wish to copy
63+
* an existing {@code HttpHeaders} or {@code WebSocketHttpHeaders} instance,
64+
* use {@link #copyOf(HttpHeaders)} instead. Note, however, that {@code copyOf()}
65+
* does not create an instance of {@code WebSocketHttpHeaders}.
66+
* <p>If the supplied {@code HttpHeaders} instance is a
67+
* {@linkplain #readOnlyHttpHeaders(HttpHeaders) read-only}
68+
* {@code HttpHeaders} wrapper, it will be unwrapped to ensure that the
69+
* {@code WebSocketHttpHeaders} instance created by this constructor is mutable.
70+
* Once the writable instance is mutated, the read-only instance is likely to
71+
* be out of sync and should be discarded.
72+
* @param httpHeaders the headers to expose
73+
* @see #copyOf(HttpHeaders)
6874
*/
69-
public WebSocketHttpHeaders(HttpHeaders headers) {
70-
this.headers = headers;
75+
public WebSocketHttpHeaders(HttpHeaders httpHeaders) {
76+
super(httpHeaders);
7177
}
7278

7379

@@ -182,132 +188,4 @@ public void setSecWebSocketVersion(@Nullable String secWebSocketVersion) {
182188
return getFirst(SEC_WEBSOCKET_VERSION);
183189
}
184190

185-
@Override
186-
public @Nullable List<String> get(String headerName) {
187-
return this.headers.get(headerName);
188-
}
189-
190-
@Override
191-
public @Nullable String getFirst(String headerName) {
192-
return this.headers.getFirst(headerName);
193-
}
194-
195-
@Override
196-
public @Nullable List<String> put(String key, List<String> value) {
197-
return this.headers.put(key, value);
198-
}
199-
200-
@Override
201-
public @Nullable List<String> putIfAbsent(String headerName, List<String> headerValues) {
202-
return this.headers.putIfAbsent(headerName, headerValues);
203-
}
204-
205-
@Override
206-
public void add(String headerName, @Nullable String headerValue) {
207-
this.headers.add(headerName, headerValue);
208-
}
209-
210-
/**
211-
* {@inheritDoc}
212-
* @since 7.0
213-
*/
214-
@Override
215-
public void addAll(String headerName, List<? extends String> headerValues) {
216-
this.headers.addAll(headerName, headerValues);
217-
}
218-
219-
@Override
220-
public void set(String headerName, @Nullable String headerValue) {
221-
this.headers.set(headerName, headerValue);
222-
}
223-
224-
@Override
225-
public void setAll(Map<String, String> values) {
226-
this.headers.setAll(values);
227-
}
228-
229-
@Override
230-
public Map<String, String> toSingleValueMap() {
231-
return this.headers.toSingleValueMap();
232-
}
233-
234-
/**
235-
* {@inheritDoc}
236-
* @since 7.0
237-
* @deprecated in favor of {@link #toSingleValueMap()} which performs a copy but
238-
* ensures that collection-iterating methods like {@code entrySet()} are
239-
* case-insensitive
240-
*/
241-
@Override
242-
@Deprecated(since = "7.0", forRemoval = true)
243-
@SuppressWarnings("removal")
244-
public Map<String, String> asSingleValueMap() {
245-
return this.headers.asSingleValueMap();
246-
}
247-
248-
/**
249-
* {@inheritDoc}
250-
* @since 7.0
251-
* @deprecated This method is provided for backward compatibility with APIs
252-
* that would only accept maps. Generally avoid using HttpHeaders as a Map
253-
* or MultiValueMap.
254-
*/
255-
@Override
256-
@Deprecated(since = "7.0", forRemoval = true)
257-
@SuppressWarnings("removal")
258-
public MultiValueMap<String, String> asMultiValueMap() {
259-
return this.headers.asMultiValueMap();
260-
}
261-
262-
@Override
263-
public boolean containsHeader(String key) {
264-
return this.headers.containsHeader(key);
265-
}
266-
267-
@Override
268-
public boolean isEmpty() {
269-
return this.headers.isEmpty();
270-
}
271-
272-
@Override
273-
public int size() {
274-
return this.headers.size();
275-
}
276-
277-
@Override
278-
public @Nullable List<String> remove(String key) {
279-
return this.headers.remove(key);
280-
}
281-
282-
@Override
283-
public void clear() {
284-
this.headers.clear();
285-
}
286-
287-
@Override
288-
public Set<String> headerNames() {
289-
return this.headers.headerNames();
290-
}
291-
292-
@Override
293-
public Set<Map.Entry<String, List<String>>> headerSet() {
294-
return this.headers.headerSet();
295-
}
296-
297-
@Override
298-
public boolean equals(@Nullable Object other) {
299-
return (this == other || (other instanceof WebSocketHttpHeaders that &&
300-
this.headers.equals(that.headers)));
301-
}
302-
303-
@Override
304-
public int hashCode() {
305-
return this.headers.hashCode();
306-
}
307-
308-
@Override
309-
public String toString() {
310-
return this.headers.toString();
311-
}
312-
313191
}

spring-websocket/src/test/java/org/springframework/web/socket/handler/WebSocketHttpHeadersTests.java

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@
2525
import org.springframework.web.socket.WebSocketHttpHeaders;
2626

2727
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2829
import static org.assertj.core.api.Assertions.entry;
30+
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
31+
import static org.springframework.http.MediaType.APPLICATION_JSON;
32+
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
33+
import static org.springframework.http.MediaType.TEXT_PLAIN;
34+
import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE;
35+
import static org.springframework.web.socket.WebSocketHttpHeaders.SEC_WEBSOCKET_EXTENSIONS;
2936

3037
/**
3138
* Tests for {@link WebSocketHttpHeaders}.
@@ -35,13 +42,101 @@
3542
*/
3643
class WebSocketHttpHeadersTests {
3744

38-
private WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
45+
private final WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
3946

4047

48+
@Test // gh-35792
49+
void constructorCopiesHeaders() {
50+
headers.setContentType(APPLICATION_JSON);
51+
assertThat(headers.getContentType()).isEqualTo(APPLICATION_JSON);
52+
53+
var copy = new WebSocketHttpHeaders(headers);
54+
assertThat(copy.getContentType()).isEqualTo(APPLICATION_JSON);
55+
56+
copy.setContentType(TEXT_PLAIN);
57+
58+
assertThat(copy.getContentType()).isEqualTo(TEXT_PLAIN);
59+
// The WebSocketHttpHeaders "copy constructor" creates an WebSocketHttpHeaders
60+
// instance that mutates the state of the original WebSocketHttpHeaders instance.
61+
assertThat(headers.getContentType()).isEqualTo(TEXT_PLAIN);
62+
}
63+
64+
@Test // gh-35792
65+
void constructorUnwrapsReadonly() {
66+
headers.setContentType(APPLICATION_JSON);
67+
68+
var readOnly = HttpHeaders.readOnlyHttpHeaders(headers);
69+
assertThat(readOnly.getContentType()).isEqualTo(APPLICATION_JSON);
70+
71+
var writable = new WebSocketHttpHeaders(readOnly);
72+
writable.setContentType(TEXT_PLAIN);
73+
74+
// content-type value is cached by ReadOnlyHttpHeaders
75+
assertThat(readOnly.getContentType()).isEqualTo(APPLICATION_JSON);
76+
assertThat(writable.getContentType()).isEqualTo(TEXT_PLAIN);
77+
assertThat(headers.getContentType()).isEqualTo(TEXT_PLAIN);
78+
}
79+
80+
@Test // gh-35792
81+
void copyOf() {
82+
headers.setContentType(APPLICATION_JSON);
83+
headers.set("X-Project", "Spring Framework");
84+
85+
assertThat(headers.getContentType()).isEqualTo(APPLICATION_JSON);
86+
assertThat(headers.toSingleValueMap()).containsOnly(
87+
entry(CONTENT_TYPE, APPLICATION_JSON_VALUE),
88+
entry("X-Project", "Spring Framework")
89+
);
90+
91+
var copy = HttpHeaders.copyOf(headers);
92+
93+
assertThat(copy.getContentType()).isEqualTo(APPLICATION_JSON);
94+
assertThat(copy.toSingleValueMap()).containsOnly(
95+
entry(CONTENT_TYPE, APPLICATION_JSON_VALUE),
96+
entry("X-Project", "Spring Framework")
97+
);
98+
99+
copy.setContentType(TEXT_PLAIN);
100+
copy.set("X-Project", "Project X");
101+
102+
assertThat(headers.getContentType()).isEqualTo(APPLICATION_JSON);
103+
assertThat(headers.toSingleValueMap()).containsOnly(
104+
entry(CONTENT_TYPE, APPLICATION_JSON_VALUE),
105+
entry("X-Project", "Spring Framework")
106+
);
107+
assertThat(copy.getContentType()).isEqualTo(TEXT_PLAIN);
108+
assertThat(copy.toSingleValueMap()).containsOnly(
109+
entry(CONTENT_TYPE, TEXT_PLAIN_VALUE),
110+
entry("X-Project", "Project X")
111+
);
112+
}
113+
114+
@Test // gh-35792
115+
void readOnlyHttpHeaders() {
116+
headers.setContentType(APPLICATION_JSON);
117+
headers.add("X-Project", "Spring Framework");
118+
119+
assertThat(headers.getContentType()).isEqualTo(APPLICATION_JSON);
120+
assertThat(headers.toSingleValueMap()).containsOnly(
121+
entry(CONTENT_TYPE, APPLICATION_JSON_VALUE),
122+
entry("X-Project", "Spring Framework")
123+
);
124+
125+
var readOnly = HttpHeaders.readOnlyHttpHeaders(headers);
126+
assertThat(readOnly.getContentType()).isEqualTo(APPLICATION_JSON);
127+
assertThat(readOnly.toSingleValueMap()).containsOnly(
128+
entry(CONTENT_TYPE, APPLICATION_JSON_VALUE),
129+
entry("X-Project", "Spring Framework")
130+
);
131+
132+
assertThatExceptionOfType(UnsupportedOperationException.class)
133+
.isThrownBy(() -> readOnly.setContentType(TEXT_PLAIN));
134+
}
135+
41136
@Test
42137
void parseWebSocketExtensions() {
43138
var extensions = List.of("x-foo-extension, x-bar-extension", "x-test-extension");
44-
this.headers.put(WebSocketHttpHeaders.SEC_WEBSOCKET_EXTENSIONS, extensions);
139+
this.headers.put(SEC_WEBSOCKET_EXTENSIONS, extensions);
45140

46141
var parsedExtensions = this.headers.getSecWebSocketExtensions();
47142
assertThat(parsedExtensions).hasSize(3);

0 commit comments

Comments
 (0)