Skip to content

Commit 5bbbc82

Browse files
committed
Consistent handling of null header values in HttpHeaders
Issue: SPR-17588
1 parent 4b45030 commit 5bbbc82

File tree

6 files changed

+69
-52
lines changed

6 files changed

+69
-52
lines changed

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

+51-33
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
7575

7676
private static final long serialVersionUID = -8578554704772377436L;
7777

78-
/**
79-
* The empty {@code HttpHeaders} instance (immutable).
80-
*/
81-
public static final HttpHeaders EMPTY =
82-
new ReadOnlyHttpHeaders(new HttpHeaders(new LinkedMultiValueMap<>(0)));
78+
8379
/**
8480
* The HTTP {@code Accept} header field name.
8581
* @see <a href="http://tools.ietf.org/html/rfc7231#section-5.3.2">Section 5.3.2 of RFC 7231</a>
@@ -381,6 +377,12 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
381377
*/
382378
public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
383379

380+
381+
/**
382+
* The empty {@code HttpHeaders} instance (immutable).
383+
*/
384+
public static final HttpHeaders EMPTY = new ReadOnlyHttpHeaders(new HttpHeaders(new LinkedMultiValueMap<>(0)));
385+
384386
/**
385387
* Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match".
386388
* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
@@ -409,15 +411,15 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
409411
* Construct a new, empty instance of the {@code HttpHeaders} object.
410412
*/
411413
public HttpHeaders() {
412-
this(CollectionUtils.toMultiValueMap(
413-
new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH)));
414+
this(CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH)));
414415
}
415416

416417
/**
417418
* Construct a new {@code HttpHeaders} instance backed by an existing map.
419+
* @since 5.1
418420
*/
419421
public HttpHeaders(MultiValueMap<String, String> headers) {
420-
Assert.notNull(headers, "headers must not be null");
422+
Assert.notNull(headers, "MultiValueMap must not be null");
421423
this.headers = headers;
422424
}
423425

@@ -445,7 +447,7 @@ public List<MediaType> getAccept() {
445447
* @since 5.0
446448
*/
447449
public void setAcceptLanguage(List<Locale.LanguageRange> languages) {
448-
Assert.notNull(languages, "'languages' must not be null");
450+
Assert.notNull(languages, "LanguageRange List must not be null");
449451
DecimalFormat decimal = new DecimalFormat("0.0", DECIMAL_FORMAT_SYMBOLS);
450452
List<String> values = languages.stream()
451453
.map(range ->
@@ -555,7 +557,7 @@ public List<HttpMethod> getAccessControlAllowMethods() {
555557
* Set the (new) value of the {@code Access-Control-Allow-Origin} response header.
556558
*/
557559
public void setAccessControlAllowOrigin(@Nullable String allowedOrigin) {
558-
set(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigin);
560+
setOrRemove(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigin);
559561
}
560562

561563
/**
@@ -614,7 +616,7 @@ public List<String> getAccessControlRequestHeaders() {
614616
* Set the (new) value of the {@code Access-Control-Request-Method} request header.
615617
*/
616618
public void setAccessControlRequestMethod(@Nullable HttpMethod requestMethod) {
617-
set(ACCESS_CONTROL_REQUEST_METHOD, (requestMethod != null ? requestMethod.name() : null));
619+
setOrRemove(ACCESS_CONTROL_REQUEST_METHOD, (requestMethod != null ? requestMethod.name() : null));
618620
}
619621

620622
/**
@@ -766,14 +768,14 @@ public void setBearerAuth(String token) {
766768
* @since 5.0.5
767769
*/
768770
public void setCacheControl(CacheControl cacheControl) {
769-
set(CACHE_CONTROL, cacheControl.getHeaderValue());
771+
setOrRemove(CACHE_CONTROL, cacheControl.getHeaderValue());
770772
}
771773

772774
/**
773775
* Set the (new) value of the {@code Cache-Control} header.
774776
*/
775777
public void setCacheControl(@Nullable String cacheControl) {
776-
set(CACHE_CONTROL, cacheControl);
778+
setOrRemove(CACHE_CONTROL, cacheControl);
777779
}
778780

779781
/**
@@ -817,7 +819,7 @@ public List<String> getConnection() {
817819
* @see #getContentDisposition()
818820
*/
819821
public void setContentDispositionFormData(String name, @Nullable String filename) {
820-
Assert.notNull(name, "'name' must not be null");
822+
Assert.notNull(name, "Name must not be null");
821823
ContentDisposition.Builder disposition = ContentDisposition.builder("form-data").name(name);
822824
if (filename != null) {
823825
disposition.filename(filename);
@@ -860,7 +862,7 @@ public ContentDisposition getContentDisposition() {
860862
* @since 5.0
861863
*/
862864
public void setContentLanguage(@Nullable Locale locale) {
863-
set(CONTENT_LANGUAGE, (locale != null ? locale.toLanguageTag() : null));
865+
setOrRemove(CONTENT_LANGUAGE, (locale != null ? locale.toLanguageTag() : null));
864866
}
865867

866868
/**
@@ -904,12 +906,12 @@ public long getContentLength() {
904906
*/
905907
public void setContentType(@Nullable MediaType mediaType) {
906908
if (mediaType != null) {
907-
Assert.isTrue(!mediaType.isWildcardType(), "'Content-Type' cannot contain wildcard type '*'");
908-
Assert.isTrue(!mediaType.isWildcardSubtype(), "'Content-Type' cannot contain wildcard subtype '*'");
909+
Assert.isTrue(!mediaType.isWildcardType(), "Content-Type cannot contain wildcard type '*'");
910+
Assert.isTrue(!mediaType.isWildcardSubtype(), "Content-Type cannot contain wildcard subtype '*'");
909911
set(CONTENT_TYPE, mediaType.toString());
910912
}
911913
else {
912-
set(CONTENT_TYPE, null);
914+
remove(CONTENT_TYPE);
913915
}
914916
}
915917

@@ -953,8 +955,11 @@ public void setETag(@Nullable String etag) {
953955
Assert.isTrue(etag.startsWith("\"") || etag.startsWith("W/"),
954956
"Invalid ETag: does not start with W/ or \"");
955957
Assert.isTrue(etag.endsWith("\""), "Invalid ETag: does not end with \"");
958+
set(ETAG, etag);
959+
}
960+
else {
961+
remove(ETAG);
956962
}
957-
set(ETAG, etag);
958963
}
959964

960965
/**
@@ -1012,7 +1017,7 @@ public void setHost(@Nullable InetSocketAddress host) {
10121017
set(HOST, value);
10131018
}
10141019
else {
1015-
set(HOST, null);
1020+
remove(HOST, null);
10161021
}
10171022
}
10181023

@@ -1160,7 +1165,7 @@ public void setLastModified(Instant lastModified) {
11601165
* @since 5.1.4
11611166
*/
11621167
public void setLastModified(ZonedDateTime lastModified) {
1163-
setZonedDateTime(LAST_MODIFIED, lastModified.withZoneSameInstant(ZoneId.of("GMT")));
1168+
setZonedDateTime(LAST_MODIFIED, lastModified.withZoneSameInstant(GMT));
11641169
}
11651170

11661171
/**
@@ -1179,7 +1184,7 @@ public long getLastModified() {
11791184
* as specified by the {@code Location} header.
11801185
*/
11811186
public void setLocation(@Nullable URI location) {
1182-
set(LOCATION, (location != null ? location.toASCIIString() : null));
1187+
setOrRemove(LOCATION, (location != null ? location.toASCIIString() : null));
11831188
}
11841189

11851190
/**
@@ -1197,7 +1202,7 @@ public URI getLocation() {
11971202
* Set the (new) value of the {@code Origin} header.
11981203
*/
11991204
public void setOrigin(@Nullable String origin) {
1200-
set(ORIGIN, origin);
1205+
setOrRemove(ORIGIN, origin);
12011206
}
12021207

12031208
/**
@@ -1212,7 +1217,7 @@ public String getOrigin() {
12121217
* Set the (new) value of the {@code Pragma} header.
12131218
*/
12141219
public void setPragma(@Nullable String pragma) {
1215-
set(PRAGMA, pragma);
1220+
setOrRemove(PRAGMA, pragma);
12161221
}
12171222

12181223
/**
@@ -1244,7 +1249,7 @@ public List<HttpRange> getRange() {
12441249
* Set the (new) value of the {@code Upgrade} header.
12451250
*/
12461251
public void setUpgrade(@Nullable String upgrade) {
1247-
set(UPGRADE, upgrade);
1252+
setOrRemove(UPGRADE, upgrade);
12481253
}
12491254

12501255
/**
@@ -1406,10 +1411,7 @@ public List<String> getValuesAsList(String headerName) {
14061411
List<String> result = new ArrayList<>();
14071412
for (String value : values) {
14081413
if (value != null) {
1409-
String[] tokens = StringUtils.tokenizeToStringArray(value, ",");
1410-
for (String token : tokens) {
1411-
result.add(token);
1412-
}
1414+
Collections.addAll(result, StringUtils.tokenizeToStringArray(value, ","));
14131415
}
14141416
}
14151417
return result;
@@ -1468,16 +1470,32 @@ protected String getFieldValues(String headerName) {
14681470
*/
14691471
protected String toCommaDelimitedString(List<String> headerValues) {
14701472
StringBuilder builder = new StringBuilder();
1471-
for (Iterator<String> it = headerValues.iterator(); it.hasNext(); ) {
1473+
for (Iterator<String> it = headerValues.iterator(); it.hasNext();) {
14721474
String val = it.next();
1473-
builder.append(val);
1474-
if (it.hasNext()) {
1475-
builder.append(", ");
1475+
if (val != null) {
1476+
builder.append(val);
1477+
if (it.hasNext()) {
1478+
builder.append(", ");
1479+
}
14761480
}
14771481
}
14781482
return builder.toString();
14791483
}
14801484

1485+
/**
1486+
* Set the given header value, or remove the header if {@code null}.
1487+
* @param headerName the header name
1488+
* @param headerValue the header value, or {@code null} for none
1489+
*/
1490+
private void setOrRemove(String headerName, @Nullable String headerValue) {
1491+
if (headerValue != null) {
1492+
set(headerName, headerValue);
1493+
}
1494+
else {
1495+
remove(headerName);
1496+
}
1497+
}
1498+
14811499

14821500
// MultiValueMap implementation
14831501

spring-web/src/main/java/org/springframework/http/ResponseEntity.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -537,10 +537,7 @@ public BodyBuilder location(URI location) {
537537

538538
@Override
539539
public BodyBuilder cacheControl(CacheControl cacheControl) {
540-
String ccValue = cacheControl.getHeaderValue();
541-
if (ccValue != null) {
542-
this.headers.setCacheControl(cacheControl.getHeaderValue());
543-
}
540+
this.headers.setCacheControl(cacheControl);
544541
return this;
545542
}
546543

spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java

+11-4
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838
import org.hamcrest.Matchers;
3939
import org.junit.Test;
4040

41-
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
42-
import static org.hamcrest.Matchers.is;
41+
import static java.time.format.DateTimeFormatter.*;
42+
import static org.hamcrest.Matchers.*;
4343
import static org.junit.Assert.*;
4444

4545
/**
@@ -355,11 +355,18 @@ public void cacheControlBuilder() {
355355
assertEquals("Invalid Cache-Control header", "no-cache", headers.getFirst("cache-control"));
356356
}
357357

358+
@Test
359+
public void cacheControlEmpty() {
360+
headers.setCacheControl(CacheControl.empty());
361+
assertNull("Invalid Cache-Control header", headers.getCacheControl());
362+
assertNull("Invalid Cache-Control header", headers.getFirst("cache-control"));
363+
}
364+
358365
@Test
359366
public void cacheControlAllValues() {
360367
headers.add(HttpHeaders.CACHE_CONTROL, "max-age=1000, public");
361368
headers.add(HttpHeaders.CACHE_CONTROL, "s-maxage=1000");
362-
assertThat(headers.getCacheControl(), is("max-age=1000, public, s-maxage=1000"));
369+
assertEquals("max-age=1000, public, s-maxage=1000", headers.getCacheControl());
363370
}
364371

365372
@Test
@@ -375,7 +382,7 @@ public void contentDisposition() {
375382

376383
@Test // SPR-11917
377384
public void getAllowEmptySet() {
378-
headers.setAllow(Collections.<HttpMethod> emptySet());
385+
headers.setAllow(Collections.emptySet());
379386
assertThat(headers.getAllow(), Matchers.emptyCollectionOf(HttpMethod.class));
380387
}
381388

spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpRequestFactoryTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -29,7 +29,7 @@
2929
*/
3030
public class SimpleClientHttpRequestFactoryTests {
3131

32-
@Test // SPR-13225
32+
@Test // SPR-13225
3333
public void headerWithNullValue() {
3434
HttpURLConnection urlConnection = mock(HttpURLConnection.class);
3535
HttpHeaders headers = new HttpHeaders();

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,7 @@ public ServerResponse.BodyBuilder location(URI location) {
177177

178178
@Override
179179
public ServerResponse.BodyBuilder cacheControl(CacheControl cacheControl) {
180-
String ccValue = cacheControl.getHeaderValue();
181-
if (ccValue != null) {
182-
this.headers.setCacheControl(cacheControl.getHeaderValue());
183-
}
180+
this.headers.setCacheControl(cacheControl);
184181
return this;
185182
}
186183

spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java

+3-5
Original file line numberDiff line numberDiff line change
@@ -345,11 +345,9 @@ public Mono<Void> handle(ServerWebExchange exchange) {
345345
}
346346

347347
// Apply cache settings, if any
348-
if (getCacheControl() != null) {
349-
String value = getCacheControl().getHeaderValue();
350-
if (value != null) {
351-
exchange.getResponse().getHeaders().setCacheControl(value);
352-
}
348+
CacheControl cacheControl = getCacheControl();
349+
if (cacheControl != null) {
350+
exchange.getResponse().getHeaders().setCacheControl(cacheControl);
353351
}
354352

355353
// Check the media type for the resource

0 commit comments

Comments
 (0)