Skip to content

Commit

Permalink
Revisit MockHttpServletResponse for Servlet 6.1
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bclozel committed Feb 24, 2025
1 parent 34129f3 commit 75329e6
Show file tree
Hide file tree
Showing 3 changed files with 686 additions and 620 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -97,7 +97,7 @@ public class MockHttpServletResponse implements HttpServletResponse {

private final ByteArrayOutputStream content = new ByteArrayOutputStream(1024);

private final ServletOutputStream outputStream = new ResponseServletOutputStream(this.content);
private @Nullable ServletOutputStream outputStream;

private @Nullable PrintWriter writer;

Expand Down Expand Up @@ -258,12 +258,17 @@ public String getCharacterEncoding() {
@Override
public ServletOutputStream getOutputStream() {
Assert.state(this.outputStreamAccessAllowed, "OutputStream access not allowed");
Assert.state(this.writer == null, "getWriter() has already been called");
if (this.outputStream == null) {
this.outputStream = new ResponseServletOutputStream(this.content);
}
return this.outputStream;
}

@Override
public PrintWriter getWriter() throws UnsupportedEncodingException {
Assert.state(this.writerAccessAllowed, "Writer access not allowed");
Assert.state(this.outputStream == null, "getOutputStream() has already been called");
if (this.writer == null) {
Writer targetWriter = new OutputStreamWriter(this.content, getCharacterEncoding());
this.writer = new ResponsePrintWriter(targetWriter);
Expand Down Expand Up @@ -365,6 +370,9 @@ else if (mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) ||
}
updateContentTypePropertyAndHeader();
}
else {
this.headers.remove(HttpHeaders.CONTENT_TYPE);
}
}

@Override
Expand Down Expand Up @@ -421,6 +429,8 @@ public void reset() {
this.headers.clear();
this.status = HttpServletResponse.SC_OK;
this.errorMessage = null;
this.writer = null;
this.outputStream = null;
}

@Override
Expand Down Expand Up @@ -680,17 +690,12 @@ private DateFormat newDateFormat() {
}

@Override
public void setHeader(String name, @Nullable String value) {
if (value == null) {
this.headers.remove(name);
}
else {
setHeaderValue(name, value);
}
public void setHeader(@Nullable String name, @Nullable String value) {
setHeaderValue(name, value);
}

@Override
public void addHeader(String name, @Nullable String value) {
public void addHeader(@Nullable String name, @Nullable String value) {
addHeaderValue(name, value);
}

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

private void setHeaderValue(String name, @Nullable Object value) {
if (value == null) {
private void setHeaderValue(@Nullable String name, @Nullable Object value) {
if (name == null) {
return;
}
boolean replaceHeader = true;
Expand All @@ -715,8 +720,8 @@ private void setHeaderValue(String name, @Nullable Object value) {
doAddHeaderValue(name, value, replaceHeader);
}

private void addHeaderValue(String name, @Nullable Object value) {
if (value == null) {
private void addHeaderValue(@Nullable String name, @Nullable Object value) {
if (name == null) {
return;
}
boolean replaceHeader = false;
Expand All @@ -726,7 +731,19 @@ private void addHeaderValue(String name, @Nullable Object value) {
doAddHeaderValue(name, value, replaceHeader);
}

private boolean setSpecialHeader(String name, Object value, boolean replaceHeader) {
private boolean setSpecialHeader(String name, @Nullable Object value, boolean replaceHeader) {
if (value == null) {
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
setContentType(null);
}
else if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
this.contentLength = 0;
}
else {
this.headers.remove(name);
}
return true;
}
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
setContentType(value.toString());
return true;
Expand Down Expand Up @@ -763,7 +780,7 @@ else if (HttpHeaders.SET_COOKIE.equalsIgnoreCase(name)) {
}
}

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

0 comments on commit 75329e6

Please sign in to comment.