Skip to content

Commit 75329e6

Browse files
committed
Revisit MockHttpServletResponse for Servlet 6.1
This commit revisits the behavior of our `MockHttpServletResponse` implementation with the javadoc clarifications applied in Servlet 6.1. Prior to this change, adding or setting an HTTP response header with a `null` name or value would not have the expected behavior: * a `null` name should have no effect instead of throwing exceptions * a `null` value when setting a header effectively removes the entry from the response headers Also, this commit ensures that `IllegalStateException` are thrown if `getWriter` is called after a previous `getOutputStream` (and vice versa). Closes gh-34467
1 parent 34129f3 commit 75329e6

File tree

3 files changed

+686
-620
lines changed

3 files changed

+686
-620
lines changed

spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java

+33-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -97,7 +97,7 @@ public class MockHttpServletResponse implements HttpServletResponse {
9797

9898
private final ByteArrayOutputStream content = new ByteArrayOutputStream(1024);
9999

100-
private final ServletOutputStream outputStream = new ResponseServletOutputStream(this.content);
100+
private @Nullable ServletOutputStream outputStream;
101101

102102
private @Nullable PrintWriter writer;
103103

@@ -258,12 +258,17 @@ public String getCharacterEncoding() {
258258
@Override
259259
public ServletOutputStream getOutputStream() {
260260
Assert.state(this.outputStreamAccessAllowed, "OutputStream access not allowed");
261+
Assert.state(this.writer == null, "getWriter() has already been called");
262+
if (this.outputStream == null) {
263+
this.outputStream = new ResponseServletOutputStream(this.content);
264+
}
261265
return this.outputStream;
262266
}
263267

264268
@Override
265269
public PrintWriter getWriter() throws UnsupportedEncodingException {
266270
Assert.state(this.writerAccessAllowed, "Writer access not allowed");
271+
Assert.state(this.outputStream == null, "getOutputStream() has already been called");
267272
if (this.writer == null) {
268273
Writer targetWriter = new OutputStreamWriter(this.content, getCharacterEncoding());
269274
this.writer = new ResponsePrintWriter(targetWriter);
@@ -365,6 +370,9 @@ else if (mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) ||
365370
}
366371
updateContentTypePropertyAndHeader();
367372
}
373+
else {
374+
this.headers.remove(HttpHeaders.CONTENT_TYPE);
375+
}
368376
}
369377

370378
@Override
@@ -421,6 +429,8 @@ public void reset() {
421429
this.headers.clear();
422430
this.status = HttpServletResponse.SC_OK;
423431
this.errorMessage = null;
432+
this.writer = null;
433+
this.outputStream = null;
424434
}
425435

426436
@Override
@@ -680,17 +690,12 @@ private DateFormat newDateFormat() {
680690
}
681691

682692
@Override
683-
public void setHeader(String name, @Nullable String value) {
684-
if (value == null) {
685-
this.headers.remove(name);
686-
}
687-
else {
688-
setHeaderValue(name, value);
689-
}
693+
public void setHeader(@Nullable String name, @Nullable String value) {
694+
setHeaderValue(name, value);
690695
}
691696

692697
@Override
693-
public void addHeader(String name, @Nullable String value) {
698+
public void addHeader(@Nullable String name, @Nullable String value) {
694699
addHeaderValue(name, value);
695700
}
696701

@@ -704,8 +709,8 @@ public void addIntHeader(String name, int value) {
704709
addHeaderValue(name, value);
705710
}
706711

707-
private void setHeaderValue(String name, @Nullable Object value) {
708-
if (value == null) {
712+
private void setHeaderValue(@Nullable String name, @Nullable Object value) {
713+
if (name == null) {
709714
return;
710715
}
711716
boolean replaceHeader = true;
@@ -715,8 +720,8 @@ private void setHeaderValue(String name, @Nullable Object value) {
715720
doAddHeaderValue(name, value, replaceHeader);
716721
}
717722

718-
private void addHeaderValue(String name, @Nullable Object value) {
719-
if (value == null) {
723+
private void addHeaderValue(@Nullable String name, @Nullable Object value) {
724+
if (name == null) {
720725
return;
721726
}
722727
boolean replaceHeader = false;
@@ -726,7 +731,19 @@ private void addHeaderValue(String name, @Nullable Object value) {
726731
doAddHeaderValue(name, value, replaceHeader);
727732
}
728733

729-
private boolean setSpecialHeader(String name, Object value, boolean replaceHeader) {
734+
private boolean setSpecialHeader(String name, @Nullable Object value, boolean replaceHeader) {
735+
if (value == null) {
736+
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
737+
setContentType(null);
738+
}
739+
else if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
740+
this.contentLength = 0;
741+
}
742+
else {
743+
this.headers.remove(name);
744+
}
745+
return true;
746+
}
730747
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
731748
setContentType(value.toString());
732749
return true;
@@ -763,7 +780,7 @@ else if (HttpHeaders.SET_COOKIE.equalsIgnoreCase(name)) {
763780
}
764781
}
765782

766-
private void doAddHeaderValue(String name, Object value, boolean replace) {
783+
private void doAddHeaderValue(String name, @Nullable Object value, boolean replace) {
767784
Assert.notNull(value, "Header value must not be null");
768785
HeaderValueHolder header = this.headers.computeIfAbsent(name, key -> new HeaderValueHolder());
769786
if (replace) {

0 commit comments

Comments
 (0)