Browse Source
Update the BasicErrorController so that it no longer needs to implement @ControllerAdvice or have an @ExceptionHandler method. A new ErrorAttributes interface is now used to obtain error details, the DefaultErrorAttributes implementation uses a HandlerExceptionResolver to obtain root exception details if the `javax.servlet.error.*` attributes are missing. This change also removes the need for the extract(...) method on ErrorController as classes such as WebRequestTraceFilter can now use the ErrorAttributes interface directly. See gh-839, gh-538 Fixes gh-843pull/874/head
15 changed files with 573 additions and 217 deletions
@ -0,0 +1,177 @@
@@ -0,0 +1,177 @@
|
||||
/* |
||||
* Copyright 2012-2014 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.autoconfigure.web; |
||||
|
||||
import java.io.PrintWriter; |
||||
import java.io.StringWriter; |
||||
import java.util.Date; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
|
||||
import javax.servlet.ServletException; |
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
import org.springframework.core.Ordered; |
||||
import org.springframework.core.annotation.Order; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.validation.BindingResult; |
||||
import org.springframework.validation.ObjectError; |
||||
import org.springframework.web.context.request.RequestAttributes; |
||||
import org.springframework.web.servlet.HandlerExceptionResolver; |
||||
import org.springframework.web.servlet.ModelAndView; |
||||
|
||||
/** |
||||
* Default implementation of {@link ErrorAttributes}. Provides the following attributes |
||||
* when possible: |
||||
* <ul> |
||||
* <li>timestamp - The time that the errors were extracted</li> |
||||
* <li>status - The status code</li> |
||||
* <li>error - The error reason</li> |
||||
* <li>exception - The class name of the root exception</li> |
||||
* <li>message - The exception message</li> |
||||
* <li>errors - Any {@link ObjectError}s from a {@link BindingResult} exception |
||||
* <li>trace - The exception stack trace</li> |
||||
* <li>path - The URL path when the exception was raised</li> |
||||
* </ul> |
||||
* |
||||
* @author Phillip Webb |
||||
* @author Dave Syer |
||||
* @since 1.1.0 |
||||
* @see ErrorAttributes |
||||
*/ |
||||
@Order(Ordered.HIGHEST_PRECEDENCE) |
||||
public class DefaulErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, |
||||
Ordered { |
||||
|
||||
private static final String ERROR_ATTRIBUTE = DefaulErrorAttributes.class.getName() |
||||
+ ".ERROR"; |
||||
|
||||
@Override |
||||
public int getOrder() { |
||||
return Ordered.HIGHEST_PRECEDENCE; |
||||
} |
||||
|
||||
@Override |
||||
public ModelAndView resolveException(HttpServletRequest request, |
||||
HttpServletResponse response, Object handler, Exception ex) { |
||||
storeErrorAttributes(request, ex); |
||||
return null; |
||||
} |
||||
|
||||
private void storeErrorAttributes(HttpServletRequest request, Exception ex) { |
||||
request.setAttribute(ERROR_ATTRIBUTE, ex); |
||||
} |
||||
|
||||
@Override |
||||
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, |
||||
boolean includeStackTrace) { |
||||
Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>(); |
||||
errorAttributes.put("timestamp", new Date()); |
||||
addStatus(errorAttributes, requestAttributes); |
||||
addErrorDetails(errorAttributes, requestAttributes, includeStackTrace); |
||||
addPath(errorAttributes, requestAttributes); |
||||
return errorAttributes; |
||||
} |
||||
|
||||
private void addStatus(Map<String, Object> errorAttributes, |
||||
RequestAttributes requestAttributes) { |
||||
Integer status = getAttribute(requestAttributes, |
||||
"javax.servlet.error.status_code"); |
||||
if (status == null) { |
||||
errorAttributes.put("status", 999); |
||||
errorAttributes.put("error", "None"); |
||||
return; |
||||
} |
||||
errorAttributes.put("status", status); |
||||
try { |
||||
errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase()); |
||||
} |
||||
catch (Exception ex) { |
||||
// Unable to obtain a reason
|
||||
errorAttributes.put("error", "Http Status " + status); |
||||
} |
||||
} |
||||
|
||||
private void addErrorDetails(Map<String, Object> errorAttributes, |
||||
RequestAttributes requestAttributes, boolean includeStackTrace) { |
||||
Throwable error = getError(requestAttributes); |
||||
if (error != null) { |
||||
while (error instanceof ServletException && error.getCause() != null) { |
||||
error = ((ServletException) error).getCause(); |
||||
} |
||||
errorAttributes.put("exception", error.getClass().getName()); |
||||
addErrorMessage(errorAttributes, error); |
||||
if (includeStackTrace) { |
||||
addStackTrace(errorAttributes, error); |
||||
} |
||||
} |
||||
else { |
||||
Object message = getAttribute(requestAttributes, |
||||
"javax.servlet.error.message"); |
||||
errorAttributes.put("message", message == null ? "No message available" |
||||
: message); |
||||
} |
||||
} |
||||
|
||||
private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) { |
||||
if (!(error instanceof BindingResult)) { |
||||
errorAttributes.put("message", error.getMessage()); |
||||
return; |
||||
} |
||||
BindingResult result = (BindingResult) error; |
||||
if (result.getErrorCount() > 0) { |
||||
errorAttributes.put("errors", result.getAllErrors()); |
||||
errorAttributes.put("message", |
||||
"Validation failed for object='" + result.getObjectName() |
||||
+ "'. Error count: " + result.getErrorCount()); |
||||
} |
||||
else { |
||||
errorAttributes.put("message", "No errors"); |
||||
} |
||||
} |
||||
|
||||
private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) { |
||||
StringWriter stackTrace = new StringWriter(); |
||||
error.printStackTrace(new PrintWriter(stackTrace)); |
||||
stackTrace.flush(); |
||||
errorAttributes.put("trace", stackTrace.toString()); |
||||
} |
||||
|
||||
private void addPath(Map<String, Object> errorAttributes, |
||||
RequestAttributes requestAttributes) { |
||||
String path = getAttribute(requestAttributes, "javax.servlet.error.request_uri"); |
||||
if (path != null) { |
||||
errorAttributes.put("path", path); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public Throwable getError(RequestAttributes requestAttributes) { |
||||
Exception exception = getAttribute(requestAttributes, ERROR_ATTRIBUTE); |
||||
if (exception == null) { |
||||
exception = getAttribute(requestAttributes, "javax.servlet.error.exception"); |
||||
} |
||||
return exception; |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
private <T> T getAttribute(RequestAttributes requestAttributes, String name) { |
||||
return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
/* |
||||
* Copyright 2012-2014 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.autoconfigure.web; |
||||
|
||||
import java.util.Map; |
||||
|
||||
import org.springframework.web.bind.annotation.ResponseBody; |
||||
import org.springframework.web.context.request.RequestAttributes; |
||||
import org.springframework.web.servlet.ModelAndView; |
||||
|
||||
/** |
||||
* Provides access to error attributes which can be logged or presented to the user. |
||||
* |
||||
* @author Phillip Webb |
||||
* @since 1.1.0 |
||||
* @see DefaulErrorAttributes |
||||
*/ |
||||
public interface ErrorAttributes { |
||||
|
||||
/** |
||||
* Returns a {@link Map} of the error attributes. The map can be used as the model of |
||||
* an error page {@link ModelAndView}, or returned as a {@link ResponseBody}. |
||||
* @param requestAttributes the source request attributes |
||||
* @param includeStackTrace if stack trace elements should be included |
||||
* @return a map of error attributes |
||||
*/ |
||||
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, |
||||
boolean includeStackTrace); |
||||
|
||||
/** |
||||
* Return the underlying cause of the error or {@code null} if the error cannot be |
||||
* extracted. |
||||
* @param requestAttributes the source request attributes |
||||
* @return the {@link Exception} that caused the error or {@code null} |
||||
*/ |
||||
public Throwable getError(RequestAttributes requestAttributes); |
||||
|
||||
} |
||||
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
/* |
||||
* Copyright 2012-2014 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.autoconfigure.web; |
||||
|
||||
import java.util.Map; |
||||
|
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
import org.springframework.beans.factory.annotation.Value; |
||||
import org.springframework.boot.test.IntegrationTest; |
||||
import org.springframework.boot.test.SpringApplicationConfiguration; |
||||
import org.springframework.boot.test.TestRestTemplate; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; |
||||
import org.springframework.test.context.web.WebAppConfiguration; |
||||
|
||||
import static org.hamcrest.Matchers.containsString; |
||||
import static org.hamcrest.Matchers.endsWith; |
||||
import static org.junit.Assert.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link BasicErrorController} using {@link IntegrationTest} that hit a real |
||||
* HTTP server. |
||||
* |
||||
* @author Phillip Webb |
||||
* @author Dave Syer |
||||
*/ |
||||
@RunWith(SpringJUnit4ClassRunner.class) |
||||
@SpringApplicationConfiguration(classes = BasicErrorControllerMockMvcTests.TestConfiguration.class) |
||||
@WebAppConfiguration |
||||
@IntegrationTest("server.port=0") |
||||
public class BasicErrorControllerIntegrationTest { |
||||
|
||||
@Value("${local.server.port}") |
||||
private int port; |
||||
|
||||
@Test |
||||
@SuppressWarnings("rawtypes") |
||||
public void testErrorForMachineClient() throws Exception { |
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity( |
||||
"http://localhost:" + this.port, Map.class); |
||||
assertThat(entity.getBody().toString(), endsWith("status=500, " |
||||
+ "error=Internal Server Error, " |
||||
+ "exception=java.lang.IllegalStateException, " + "message=Expected!, " |
||||
+ "path=/}")); |
||||
} |
||||
|
||||
@Test |
||||
@SuppressWarnings("rawtypes") |
||||
public void testBindingExceptionForMachineClient() throws Exception { |
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity( |
||||
"http://localhost:" + this.port + "/bind", Map.class); |
||||
String resp = entity.getBody().toString(); |
||||
assertThat(resp, containsString("Error count: 1")); |
||||
assertThat(resp, containsString("errors=[{codes=")); |
||||
assertThat(resp, containsString("org.springframework.validation.BindException")); |
||||
} |
||||
} |
||||
@ -0,0 +1,176 @@
@@ -0,0 +1,176 @@
|
||||
/* |
||||
* Copyright 2012-2014 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.autoconfigure.web; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.Date; |
||||
import java.util.Map; |
||||
|
||||
import javax.servlet.ServletException; |
||||
|
||||
import org.junit.Test; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.mock.web.MockHttpServletRequest; |
||||
import org.springframework.validation.BindException; |
||||
import org.springframework.validation.BindingResult; |
||||
import org.springframework.validation.MapBindingResult; |
||||
import org.springframework.validation.ObjectError; |
||||
import org.springframework.web.context.request.RequestAttributes; |
||||
import org.springframework.web.context.request.ServletRequestAttributes; |
||||
import org.springframework.web.servlet.ModelAndView; |
||||
|
||||
import static org.hamcrest.Matchers.equalTo; |
||||
import static org.hamcrest.Matchers.instanceOf; |
||||
import static org.hamcrest.Matchers.nullValue; |
||||
import static org.hamcrest.Matchers.sameInstance; |
||||
import static org.hamcrest.Matchers.startsWith; |
||||
import static org.junit.Assert.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link DefaulErrorAttributes}. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
public class DefaultErrorAttributesTests { |
||||
|
||||
private DefaulErrorAttributes errorAttributes = new DefaulErrorAttributes(); |
||||
|
||||
private MockHttpServletRequest request = new MockHttpServletRequest(); |
||||
|
||||
private RequestAttributes requestAttributes = new ServletRequestAttributes( |
||||
this.request); |
||||
|
||||
@Test |
||||
public void includeTimeStamp() throws Exception { |
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes( |
||||
this.requestAttributes, false); |
||||
assertThat(attributes.get("timestamp"), instanceOf(Date.class)); |
||||
} |
||||
|
||||
@Test |
||||
public void specificStatusCode() throws Exception { |
||||
this.request.setAttribute("javax.servlet.error.status_code", 404); |
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes( |
||||
this.requestAttributes, false); |
||||
assertThat(attributes.get("error"), |
||||
equalTo((Object) HttpStatus.NOT_FOUND.getReasonPhrase())); |
||||
assertThat(attributes.get("status"), equalTo((Object) 404)); |
||||
} |
||||
|
||||
@Test |
||||
public void missingStatusCode() throws Exception { |
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes( |
||||
this.requestAttributes, false); |
||||
assertThat(attributes.get("error"), equalTo((Object) "None")); |
||||
assertThat(attributes.get("status"), equalTo((Object) 999)); |
||||
} |
||||
|
||||
@Test |
||||
public void mvcError() throws Exception { |
||||
RuntimeException ex = new RuntimeException("Test"); |
||||
ModelAndView modelAndView = this.errorAttributes.resolveException(this.request, |
||||
null, null, ex); |
||||
this.request.setAttribute("javax.servlet.error.exception", new RuntimeException( |
||||
"Ignored")); |
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes( |
||||
this.requestAttributes, false); |
||||
assertThat(this.errorAttributes.getError(this.requestAttributes), |
||||
sameInstance((Object) ex)); |
||||
assertThat(modelAndView, nullValue()); |
||||
assertThat(attributes.get("exception"), |
||||
equalTo((Object) RuntimeException.class.getName())); |
||||
assertThat(attributes.get("message"), equalTo((Object) "Test")); |
||||
} |
||||
|
||||
@Test |
||||
public void servletError() throws Exception { |
||||
RuntimeException ex = new RuntimeException("Test"); |
||||
this.request.setAttribute("javax.servlet.error.exception", ex); |
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes( |
||||
this.requestAttributes, false); |
||||
assertThat(this.errorAttributes.getError(this.requestAttributes), |
||||
sameInstance((Object) ex)); |
||||
assertThat(attributes.get("exception"), |
||||
equalTo((Object) RuntimeException.class.getName())); |
||||
assertThat(attributes.get("message"), equalTo((Object) "Test")); |
||||
} |
||||
|
||||
@Test |
||||
public void servletMessage() throws Exception { |
||||
this.request.setAttribute("javax.servlet.error.message", "Test"); |
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes( |
||||
this.requestAttributes, false); |
||||
assertThat(attributes.get("exception"), nullValue()); |
||||
assertThat(attributes.get("message"), equalTo((Object) "Test")); |
||||
} |
||||
|
||||
@Test |
||||
public void unwrapServletException() throws Exception { |
||||
RuntimeException ex = new RuntimeException("Test"); |
||||
ServletException wrapped = new ServletException(new ServletException(ex)); |
||||
this.request.setAttribute("javax.servlet.error.exception", wrapped); |
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes( |
||||
this.requestAttributes, false); |
||||
assertThat(this.errorAttributes.getError(this.requestAttributes), |
||||
sameInstance((Object) wrapped)); |
||||
assertThat(attributes.get("exception"), |
||||
equalTo((Object) RuntimeException.class.getName())); |
||||
assertThat(attributes.get("message"), equalTo((Object) "Test")); |
||||
} |
||||
|
||||
@Test |
||||
public void extractBindingResultErrors() throws Exception { |
||||
BindingResult bindingResult = new MapBindingResult(Collections.singletonMap("a", |
||||
"b"), "objectName"); |
||||
bindingResult.addError(new ObjectError("c", "d")); |
||||
BindException ex = new BindException(bindingResult); |
||||
this.request.setAttribute("javax.servlet.error.exception", ex); |
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes( |
||||
this.requestAttributes, false); |
||||
assertThat(attributes.get("message"), equalTo((Object) ("Validation failed for " |
||||
+ "object='objectName'. Error count: 1"))); |
||||
assertThat(attributes.get("errors"), |
||||
equalTo((Object) bindingResult.getAllErrors())); |
||||
} |
||||
|
||||
@Test |
||||
public void trace() throws Exception { |
||||
RuntimeException ex = new RuntimeException("Test"); |
||||
this.request.setAttribute("javax.servlet.error.exception", ex); |
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes( |
||||
this.requestAttributes, true); |
||||
assertThat(attributes.get("trace").toString(), startsWith("java.lang")); |
||||
} |
||||
|
||||
@Test |
||||
public void noTrace() throws Exception { |
||||
RuntimeException ex = new RuntimeException("Test"); |
||||
this.request.setAttribute("javax.servlet.error.exception", ex); |
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes( |
||||
this.requestAttributes, false); |
||||
assertThat(attributes.get("trace"), nullValue()); |
||||
} |
||||
|
||||
@Test |
||||
public void path() throws Exception { |
||||
this.request.setAttribute("javax.servlet.error.request_uri", "path"); |
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes( |
||||
this.requestAttributes, false); |
||||
assertThat(attributes.get("path"), equalTo((Object) "path")); |
||||
|
||||
} |
||||
} |
||||
Loading…
Reference in new issue