Skip to content

Commit 72ef1d6

Browse files
committed
Update ErrorPageFilter so it won’t try to forward a committed response
In some scenarios, the ErrorPageFilter will want to forward the request to an error page but the response has already been committed. One common cause of this is when the filter’s running on WAS. WAS calls flushBuffer() (which commits the response), upon a clean exit from a servlet’s service method. Previously, the filter would attempt the forward, even if the response was committed. This would result in an IllegalStateException and a possibly incomplete response that may also have an incorrect status code. This commit updates the ErrorPageFilter to check to see if the response has already been committed before it attempts to forward the request to the error page. If the response has already been committed, the filter logs an error and allows the container’s normal handling to kick in. This prevents an IllegalStateException from being thrown. This commit also updates the response wrapper to keep track of when sendError has been called. Now, when flushBuffer is called, if sendError has been called, the wrapper calls sendError on the wrapped response. This prevents the wrapper from suppressing an error when the response is committed before the request handling returns to the error page filter. Closes gh-1575
1 parent 258059e commit 72ef1d6

File tree

3 files changed

+119
-28
lines changed

3 files changed

+119
-28
lines changed

spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -952,8 +952,8 @@ Spring Boot includes auto-configuration support for the following templating eng
952952
* http://www.thymeleaf.org[Thymeleaf]
953953
* http://velocity.apache.org[Velocity]
954954

955-
When you're using one of these templating engines with the default configuration, your templates
956-
will be picked up automatically from `src/main/resources/templates`.
955+
When you're using one of these templating engines with the default configuration, your
956+
templates will be picked up automatically from `src/main/resources/templates`.
957957

958958
TIP: JSPs should be avoided if possible, there are several
959959
<<boot-features-jsp-limitations, known limitations>> when using them with embedded
@@ -988,36 +988,45 @@ support a uniform Java DSL for customizing the error handling. For example:
988988
989989
@Override
990990
public void customize(ConfigurableEmbeddedServletContainer container) {
991-
container.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
991+
container.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
992992
}
993993
994994
}
995995
----
996996

997-
You can also use regular Spring MVC features like http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-exception-handlers[`@ExceptionHandler`
998-
methods] and http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-controller-advice[`@ControllerAdvice`].
999-
The `ErrorController` will then pick up any unhandled exceptions.
997+
You can also use regular Spring MVC features like
998+
{spring-reference}/#mvc-exceptionhandlers[`@ExceptionHandler` methods] and
999+
{spring-reference}/#mvc-ann-controller-advice[`@ControllerAdvice`]. The `ErrorController`
1000+
will then pick up any unhandled exceptions.
10001001

10011002
N.B. if you register an `ErrorPage` with a path that will end up being handled by a
1002-
`Filter` (e.g. as is common with some non-Spring web frameworks, like Jersey and Wicket),
1003+
`Filter` (e.g. as is common with some non-Spring web frameworks, like Jersey and Wicket),
10031004
then the `Filter` has to be explicitly registered as an `ERROR` dispatcher, e.g.
10041005

10051006
[source,java,indent=0,subs="verbatim,quotes,attributes"]
10061007
----
10071008
@Bean
10081009
public FilterRegistrationBean myFilter() {
1009-
1010-
FilterRegistrationBean registration = new FilterRegistrationBean();
1011-
registration.setFilter(new MyFilter());
1012-
...
1013-
registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
1014-
return registration;
1015-
1010+
FilterRegistrationBean registration = new FilterRegistrationBean();
1011+
registration.setFilter(new MyFilter());
1012+
...
1013+
registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
1014+
return registration;
10161015
}
10171016
----
10181017

10191018
(the default `FilterRegistrationBean` does not include the `ERROR` dispatcher type).
10201019

1020+
[[boot-features-error-handling-websphere]]
1021+
===== Error Handling on WebSphere Application Server
1022+
When deployed to a servlet container, a Spring Boot uses its error page filter to
1023+
forward a request with an error status to the appropriate error page. The request can
1024+
only be forwarded to the correct error page if the response has not already been
1025+
committed. By default, WebSphere Application Server 8.0 and later commits the response
1026+
upon successful completion of a servlet's service method. You should disable this
1027+
behaviour by setting `com.ibm.ws.webcontainer.invokeFlushAfterService` to `false`
1028+
1029+
10211030

10221031

10231032
[[boot-features-embedded-container]]

spring-boot/src/main/java/org/springframework/boot/context/web/ErrorPageFilter.java

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
*
5353
* @author Dave Syer
5454
* @author Phillip Webb
55+
* @author Andy Wilkinson
5556
*/
5657
@Component
5758
@Order(Ordered.HIGHEST_PRECEDENCE)
@@ -124,6 +125,12 @@ else if (!request.isAsyncStarted()) {
124125
private void handleErrorStatus(HttpServletRequest request,
125126
HttpServletResponse response, int status, String message)
126127
throws ServletException, IOException {
128+
129+
if (response.isCommitted()) {
130+
handleCommittedResponse(request, null);
131+
return;
132+
}
133+
127134
String errorPath = getErrorPath(this.statuses, status);
128135
if (errorPath == null) {
129136
response.sendError(status, message);
@@ -132,6 +139,7 @@ private void handleErrorStatus(HttpServletRequest request,
132139
response.setStatus(status);
133140
setErrorAttributes(request, status, message);
134141
request.getRequestDispatcher(errorPath).forward(request, response);
142+
135143
}
136144

137145
private void handleException(HttpServletRequest request,
@@ -143,33 +151,49 @@ private void handleException(HttpServletRequest request,
143151
rethrow(ex);
144152
return;
145153
}
154+
if (response.isCommitted()) {
155+
handleCommittedResponse(request, ex);
156+
return;
157+
}
158+
159+
forwardToErrorPage(errorPath, request, wrapped, ex);
160+
}
161+
162+
private void forwardToErrorPage(String path, HttpServletRequest request,
163+
HttpServletResponse response, Throwable ex) throws ServletException,
164+
IOException {
165+
146166
if (logger.isErrorEnabled()) {
147167
String message = "Forwarding to error page from request ["
148168
+ request.getServletPath() + request.getPathInfo()
149169
+ "] due to exception [" + ex.getMessage() + "]";
150170
logger.error(message, ex);
151171
}
172+
152173
setErrorAttributes(request, 500, ex.getMessage());
153174
request.setAttribute(ERROR_EXCEPTION, ex);
154-
request.setAttribute(ERROR_EXCEPTION_TYPE, type.getName());
155-
forwardToErrorPage(errorPath, request, wrapped, ex);
175+
request.setAttribute(ERROR_EXCEPTION_TYPE, ex.getClass().getName());
176+
177+
response.reset();
178+
response.sendError(500, ex.getMessage());
179+
request.getRequestDispatcher(path).forward(request, response);
156180
}
157181

158-
private void forwardToErrorPage(String path, HttpServletRequest request,
159-
HttpServletResponse response, Throwable ex) throws ServletException,
160-
IOException {
161-
if (response.isCommitted()) {
162-
String message = "Cannot forward to error page for" + request.getRequestURI()
163-
+ " (response is committed), so this response may have "
164-
+ "the wrong status code";
182+
private void handleCommittedResponse(HttpServletRequest request, Throwable ex) {
183+
String message = "Cannot forward to error page for request to "
184+
+ request.getRequestURI() + " as the response has already been"
185+
+ " committed. As a result, the response may have the wrong status"
186+
+ " code. If your application is running on WebSphere Application"
187+
+ " Server you may be able to resolve this problem by setting "
188+
+ " com.ibm.ws.webcontainer.invokeFlushAfterService to false";
189+
if (ex == null) {
190+
logger.error(message);
191+
}
192+
else {
165193
// User might see the error page without all the data here but throwing the
166194
// exception isn't going to help anyone (we'll log it to be on the safe side)
167195
logger.error(message, ex);
168-
return;
169196
}
170-
response.reset();
171-
response.sendError(500, ex.getMessage());
172-
request.getRequestDispatcher(path).forward(request, response);
173197
}
174198

175199
private String getErrorPath(Map<Integer, String> map, Integer status) {
@@ -243,6 +267,8 @@ private static class ErrorWrapperResponse extends HttpServletResponseWrapper {
243267

244268
private String message;
245269

270+
private boolean errorToSend;
271+
246272
public ErrorWrapperResponse(HttpServletResponse response) {
247273
super(response);
248274
this.status = response.getStatus();
@@ -257,13 +283,24 @@ public void sendError(int status) throws IOException {
257283
public void sendError(int status, String message) throws IOException {
258284
this.status = status;
259285
this.message = message;
286+
287+
this.errorToSend = true;
260288
}
261289

262290
@Override
263291
public int getStatus() {
264292
return this.status;
265293
}
266294

295+
@Override
296+
public void flushBuffer() throws IOException {
297+
if (this.errorToSend && !isCommitted()) {
298+
((HttpServletResponse) getResponse())
299+
.sendError(this.status, this.message);
300+
}
301+
super.flushBuffer();
302+
}
303+
267304
public String getMessage() {
268305
return this.message;
269306
}

spring-boot/src/test/java/org/springframework/boot/context/web/ErrorPageFilterTests.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import org.springframework.mock.web.MockHttpServletResponse;
3535

3636
import static org.hamcrest.Matchers.equalTo;
37+
import static org.hamcrest.Matchers.is;
38+
import static org.hamcrest.Matchers.nullValue;
3739
import static org.junit.Assert.assertFalse;
3840
import static org.junit.Assert.assertNotNull;
3941
import static org.junit.Assert.assertThat;
@@ -43,6 +45,7 @@
4345
* Tests for {@link ErrorPageFilter}.
4446
*
4547
* @author Dave Syer
48+
* @author Andy Wilkinson
4649
*/
4750
public class ErrorPageFilterTests {
4851

@@ -61,6 +64,7 @@ public void notAnError() throws Exception {
6164
assertThat(((HttpServletResponseWrapper) this.chain.getResponse()).getResponse(),
6265
equalTo((ServletResponse) this.response));
6366
assertTrue(this.response.isCommitted());
67+
assertThat(this.response.getForwardedUrl(), is(nullValue()));
6468
}
6569

6670
@Test
@@ -83,6 +87,7 @@ public void doFilter(ServletRequest request, ServletResponse response)
8387
assertThat(wrapper.getStatus(), equalTo(401));
8488
// The real response has to be 401 as well...
8589
assertThat(this.response.getStatus(), equalTo(401));
90+
assertThat(this.response.getForwardedUrl(), equalTo("/error"));
8691
}
8792

8893
@Test
@@ -103,11 +108,12 @@ public void doFilter(ServletRequest request, ServletResponse response)
103108
equalTo((ServletResponse) this.response));
104109
assertThat(((HttpServletResponseWrapper) this.chain.getResponse()).getStatus(),
105110
equalTo(400));
111+
assertThat(this.response.getForwardedUrl(), is(nullValue()));
106112
assertTrue(this.response.isCommitted());
107113
}
108114

109115
@Test
110-
public void responseUncommitted() throws Exception {
116+
public void responseUncommittedWithoutErrorPage() throws Exception {
111117
this.chain = new MockFilterChain() {
112118
@Override
113119
public void doFilter(ServletRequest request, ServletResponse response)
@@ -122,6 +128,7 @@ public void doFilter(ServletRequest request, ServletResponse response)
122128
equalTo((ServletResponse) this.response));
123129
assertThat(((HttpServletResponseWrapper) this.chain.getResponse()).getStatus(),
124130
equalTo(400));
131+
assertThat(this.response.getForwardedUrl(), is(nullValue()));
125132
assertTrue(this.response.isCommitted());
126133
}
127134

@@ -159,6 +166,7 @@ public void doFilter(ServletRequest request, ServletResponse response)
159166
assertThat(this.request.getAttribute(RequestDispatcher.ERROR_MESSAGE),
160167
equalTo((Object) "BAD"));
161168
assertTrue(this.response.isCommitted());
169+
assertThat(this.response.getForwardedUrl(), equalTo("/error"));
162170
}
163171

164172
@Test
@@ -180,6 +188,26 @@ public void doFilter(ServletRequest request, ServletResponse response)
180188
assertThat(this.request.getAttribute(RequestDispatcher.ERROR_MESSAGE),
181189
equalTo((Object) "BAD"));
182190
assertTrue(this.response.isCommitted());
191+
assertThat(this.response.getForwardedUrl(), equalTo("/400"));
192+
}
193+
194+
@Test
195+
public void statusErrorWithCommittedResponse() throws Exception {
196+
this.filter.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
197+
this.chain = new MockFilterChain() {
198+
@Override
199+
public void doFilter(ServletRequest request, ServletResponse response)
200+
throws IOException, ServletException {
201+
((HttpServletResponse) response).sendError(400, "BAD");
202+
response.flushBuffer();
203+
super.doFilter(request, response);
204+
}
205+
};
206+
this.filter.doFilter(this.request, this.response, this.chain);
207+
assertThat(((HttpServletResponseWrapper) this.chain.getResponse()).getStatus(),
208+
equalTo(400));
209+
assertTrue(this.response.isCommitted());
210+
assertThat(this.response.getForwardedUrl(), is(nullValue()));
183211
}
184212

185213
@Test
@@ -203,6 +231,23 @@ public void doFilter(ServletRequest request, ServletResponse response)
203231
assertThat(this.request.getAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE),
204232
equalTo((Object) RuntimeException.class.getName()));
205233
assertTrue(this.response.isCommitted());
234+
assertThat(this.response.getForwardedUrl(), equalTo("/500"));
235+
}
236+
237+
@Test
238+
public void exceptionErrorWithCommittedResponse() throws Exception {
239+
this.filter.addErrorPages(new ErrorPage(RuntimeException.class, "/500"));
240+
this.chain = new MockFilterChain() {
241+
@Override
242+
public void doFilter(ServletRequest request, ServletResponse response)
243+
throws IOException, ServletException {
244+
super.doFilter(request, response);
245+
response.flushBuffer();
246+
throw new RuntimeException("BAD");
247+
}
248+
};
249+
this.filter.doFilter(this.request, this.response, this.chain);
250+
assertThat(this.response.getForwardedUrl(), is(nullValue()));
206251
}
207252

208253
@Test

0 commit comments

Comments
 (0)