Commit 30ac768c authored by Phillip Webb's avatar Phillip Webb

Remove @ControllerAdvice from BasicErrorController

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-843
parent 5dd77a72
......@@ -34,7 +34,7 @@ import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.web.ErrorController;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainer;
......@@ -125,12 +125,11 @@ public class EndpointWebMvcChildContextConfiguration {
/*
* The error controller is present but not mapped as an endpoint in this context
* because of the DispatcherServlet having had it's HandlerMapping explicitly
* disabled. So this tiny shim exposes the same feature but only for machine
* endpoints.
* disabled. So we expose the same feature but only for machine endpoints.
*/
@Bean
public ManagementErrorEndpoint errorEndpoint(final ErrorController controller) {
return new ManagementErrorEndpoint(this.errorPath, controller);
public ManagementErrorEndpoint errorEndpoint(final ErrorAttributes errorAttributes) {
return new ManagementErrorEndpoint(this.errorPath, errorAttributes);
}
@Configuration
......
......@@ -27,7 +27,7 @@ import org.springframework.boot.actuate.trace.WebRequestTraceFilter;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.web.BasicErrorController;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.DispatcherServlet;
......@@ -45,7 +45,7 @@ public class TraceWebFilterAutoConfiguration {
private TraceRepository traceRepository;
@Autowired(required = false)
private BasicErrorController errorController;
private ErrorAttributes errorAttributes;
@Value("${management.dump_requests:false}")
private boolean dumpRequests;
......@@ -54,8 +54,8 @@ public class TraceWebFilterAutoConfiguration {
public WebRequestTraceFilter webRequestLoggingFilter(BeanFactory beanFactory) {
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.traceRepository);
filter.setDumpRequests(this.dumpRequests);
if (this.errorController != null) {
filter.setErrorController(this.errorController);
if (this.errorAttributes != null) {
filter.setErrorAttributes(this.errorAttributes);
}
return filter;
}
......
......@@ -19,12 +19,12 @@ package org.springframework.boot.actuate.endpoint.mvc;
import java.util.Map;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.autoconfigure.web.ErrorController;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
/**
......@@ -37,21 +37,21 @@ import org.springframework.web.context.request.RequestContextHolder;
@ConfigurationProperties(prefix = "error")
public class ManagementErrorEndpoint implements MvcEndpoint {
private final ErrorController controller;
private final ErrorAttributes errorAttributes;
private final String path;
public ManagementErrorEndpoint(String path, ErrorController controller) {
Assert.notNull(controller, "Controller must not be null");
public ManagementErrorEndpoint(String path, ErrorAttributes errorAttributes) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.path = path;
this.controller = controller;
this.errorAttributes = errorAttributes;
}
@RequestMapping
@ResponseBody
public Map<String, Object> invoke() {
RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
return this.controller.extract(attributes, false, true);
return this.errorAttributes.getErrorAttributes(
RequestContextHolder.currentRequestAttributes(), false);
}
@Override
......
......@@ -34,8 +34,9 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.web.BasicErrorController;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.core.Ordered;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.fasterxml.jackson.core.JsonProcessingException;
......@@ -58,7 +59,7 @@ public class WebRequestTraceFilter implements Filter, Ordered {
private final ObjectMapper objectMapper = new ObjectMapper();
private BasicErrorController errorController;
private ErrorAttributes errorAttributes;
/**
* @param traceRepository
......@@ -156,13 +157,13 @@ public class WebRequestTraceFilter implements Filter, Ordered {
trace.put("method", request.getMethod());
trace.put("path", request.getRequestURI());
trace.put("headers", allHeaders);
Throwable error = (Throwable) request
Throwable exception = (Throwable) request
.getAttribute("javax.servlet.error.exception");
if (error != null) {
if (this.errorController != null) {
trace.put("error", this.errorController.extract(
new ServletRequestAttributes(request), true, false));
}
if (exception != null && this.errorAttributes != null) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
Map<String, Object> error = this.errorAttributes.getErrorAttributes(
requestAttributes, true);
trace.put("error", error);
}
return trace;
}
......@@ -175,8 +176,8 @@ public class WebRequestTraceFilter implements Filter, Ordered {
public void destroy() {
}
public void setErrorController(BasicErrorController errorController) {
this.errorController = errorController;
public void setErrorAttributes(ErrorAttributes errorAttributes) {
this.errorAttributes = errorAttributes;
}
}
/*
* Copyright 2012-2013 the original author or authors.
* 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.
......@@ -19,7 +19,7 @@ package org.springframework.boot.actuate.trace;
import java.util.Map;
import org.junit.Test;
import org.springframework.boot.autoconfigure.web.BasicErrorController;
import org.springframework.boot.autoconfigure.web.DefaulErrorAttributes;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
......@@ -76,7 +76,7 @@ public class WebRequestTraceFilterTests {
@Test
public void filterHasError() {
this.filter.setErrorController(new BasicErrorController());
this.filter.setErrorAttributes(new DefaulErrorAttributes());
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
MockHttpServletResponse response = new MockHttpServletResponse();
response.setStatus(500);
......
......@@ -20,9 +20,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
......@@ -35,9 +32,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
......@@ -51,13 +46,10 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.header.writers.HstsHeaderWriter;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.support.RequestDataValueProcessor;
/**
......@@ -109,18 +101,6 @@ public class SpringBootWebSecurityConfiguration {
return new IgnoredPathsWebSecurityConfigurerAdapter();
}
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
protected static class SecurityExceptionRethrowingAdvice {
@ExceptionHandler({ AccessDeniedException.class, AuthenticationException.class })
public void handle(HttpServletRequest request, HttpServletResponse response,
Exception e) throws Exception {
throw e;
}
}
public static void configureHeaders(HeadersConfigurer<?> configurer,
SecurityProperties.Headers headers) throws Exception {
if (headers.getHsts() != Headers.HSTS.none) {
......
......@@ -16,188 +16,89 @@
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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
/**
* Basic global error {@link Controller}, rendering servlet container error codes and
* messages where available. More specific errors can be handled either using Spring MVC
* abstractions (e.g. {@code @ExceptionHandler}) or by adding servlet
* {@link AbstractEmbeddedServletContainerFactory#setErrorPages(java.util.Set) container
* error pages}.
* Basic global error {@link Controller}, rendering {@link ErrorAttributes}. More specific
* errors can be handled either using Spring MVC abstractions (e.g.
* {@code @ExceptionHandler}) or by adding servlet
* {@link AbstractEmbeddedServletContainerFactory#setErrorPages container error pages}.
*
* @author Dave Syer
* @author Phillip Webb
* @see ErrorAttributes
*/
@Controller
@ControllerAdvice
@Order(0)
public class BasicErrorController implements ErrorController {
private static final String ERROR_KEY = "error";
private final Log logger = LogFactory.getLog(BasicErrorController.class);
private DefaultHandlerExceptionResolver resolver = new DefaultHandlerExceptionResolver();
private ResponseStatusExceptionResolver statuses = new ResponseStatusExceptionResolver();
@Value("${error.path:/error}")
private String errorPath;
private final ErrorAttributes errorAttributes;
public BasicErrorController(ErrorAttributes errorAttributes) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
}
@Override
public String getErrorPath() {
return this.errorPath;
}
@ExceptionHandler(Exception.class)
public void handle(HttpServletRequest request, HttpServletResponse response,
Exception e) throws Exception {
if (this.statuses.resolveException(request, response, null, e) == null) {
this.resolver.resolveException(request, response, null, e);
}
if (response.getStatus() == HttpServletResponse.SC_OK) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
// There's only one exception so it's easier for the error controller to identify
// it this way...
request.setAttribute(ErrorController.class.getName(), e);
if (e instanceof BindException) {
// ... but other error pages might be looking for it here as well
request.setAttribute(
BindingResult.MODEL_KEY_PREFIX + ((BindException) e).getObjectName(),
e);
}
}
@RequestMapping(value = "${error.path:/error}", produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request) {
Map<String, Object> map = extract(new ServletRequestAttributes(request), false,
false);
return new ModelAndView(ERROR_KEY, map);
return new ModelAndView("error", getErrorAttributes(request, false));
}
@RequestMapping(value = "${error.path:/error}")
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
ServletRequestAttributes attributes = new ServletRequestAttributes(request);
String trace = request.getParameter("trace");
Map<String, Object> extracted = extract(attributes,
trace != null && !"false".equals(trace.toLowerCase()), true);
HttpStatus statusCode = getStatus((Integer) extracted.get("status"));
return new ResponseEntity<Map<String, Object>>(extracted, statusCode);
Map<String, Object> body = getErrorAttributes(request, getTraceParameter(request));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}
private HttpStatus getStatus(Integer value) {
try {
return HttpStatus.valueOf(value);
}
catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
private boolean getTraceParameter(HttpServletRequest request) {
String parameter = request.getParameter("trace");
if (parameter == null) {
return false;
}
return !"false".equals(parameter.toLowerCase());
}
@Override
public Map<String, Object> extract(RequestAttributes attributes, boolean trace,
boolean log) {
Map<String, Object> map = new LinkedHashMap<String, Object>();
map.put("timestamp", new Date());
try {
Throwable error = (Throwable) attributes.getAttribute(
ErrorController.class.getName(), RequestAttributes.SCOPE_REQUEST);
Object obj = attributes.getAttribute("javax.servlet.error.status_code",
RequestAttributes.SCOPE_REQUEST);
int status = 999;
if (obj != null) {
status = (Integer) obj;
map.put(ERROR_KEY, HttpStatus.valueOf(status).getReasonPhrase());
}
else {
map.put(ERROR_KEY, "None");
}
map.put("status", status);
if (error == null) {
error = (Throwable) attributes.getAttribute(
"javax.servlet.error.exception", RequestAttributes.SCOPE_REQUEST);
}
if (error != null) {
while (error instanceof ServletException && error.getCause() != null) {
error = ((ServletException) error).getCause();
}
map.put("exception", error.getClass().getName());
addMessage(map, error);
if (trace) {
StringWriter stackTrace = new StringWriter();
error.printStackTrace(new PrintWriter(stackTrace));
stackTrace.flush();
map.put("trace", stackTrace.toString());
}
if (log) {
this.logger.error(error);
}
}
else {
Object message = attributes.getAttribute("javax.servlet.error.message",
RequestAttributes.SCOPE_REQUEST);
map.put("message", message == null ? "No message available" : message);
}
String path = (String) attributes.getAttribute(
"javax.servlet.error.request_uri", RequestAttributes.SCOPE_REQUEST);
map.put("path", path == null ? "No path available" : path);
return map;
}
catch (Exception ex) {
map.put(ERROR_KEY, ex.getClass().getName());
map.put("message", ex.getMessage());
if (log) {
this.logger.error(ex);
}
return map;
}
private Map<String, Object> getErrorAttributes(HttpServletRequest request,
boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
return this.errorAttributes.getErrorAttributes(requestAttributes,
includeStackTrace);
}
protected void addMessage(Map<String, Object> map, Throwable error) {
if (error instanceof BindingResult) {
BindingResult result = (BindingResult) error;
if (result.getErrorCount() > 0) {
map.put("errors", result.getAllErrors());
map.put("message",
"Validation failed for object='" + result.getObjectName()
+ "'. Error count: " + result.getErrorCount());
private HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
if (statusCode != null) {
try {
return HttpStatus.valueOf(statusCode);
}
else {
map.put("message", "No errors");
catch (Exception ex) {
}
}
else {
map.put("message", error.getMessage());
}
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
/*
* 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);
}
}
/*
* 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);
}
......@@ -16,14 +16,11 @@
package org.springframework.boot.autoconfigure.web;
import java.util.Map;
import org.springframework.stereotype.Controller;
import org.springframework.web.context.request.RequestAttributes;
/**
* Marker interface used to indicate that a {@link Controller @Controller} is used to
* render errors.
* render errors. Primarily used to know the error paths that will not need to be secured.
*
* @author Phillip Webb
*/
......@@ -34,14 +31,4 @@ public interface ErrorController {
*/
public String getErrorPath();
/**
* Extract a useful model of the error from the request attributes.
* @param attributes the request attributes
* @param trace flag to indicate that stack trace information should be included
* @param log flag to indicate that an error should be logged
* @return a model containing error messages and codes etc.
*/
public Map<String, Object> extract(RequestAttributes attributes, boolean trace,
boolean log);
}
......@@ -72,10 +72,16 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom
@Value("${error.path:/error}")
private String errorPath = "/error";
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaulErrorAttributes errorAttributes() {
return new DefaulErrorAttributes();
}
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController() {
return new BasicErrorController();
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes);
}
@Override
......@@ -136,6 +142,9 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom
}
/**
* Simple {@link View} implementation that resolves variables as SpEL expressions.
*/
private static class SpelView implements View {
private final String template;
......@@ -156,8 +165,13 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom
@Override
public String resolvePlaceholder(String name) {
Expression expression = SpelView.this.parser.parseExpression(name);
Object value = expression.getValue(SpelView.this.context);
return value == null ? null : value.toString();
try {
Object value = expression.getValue(SpelView.this.context);
return (value == null ? null : value.toString());
}
catch (Exception ex) {
return null;
}
}
};
}
......
/*
* Copyright 2012-2013 the original author or authors.
* 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.
......@@ -26,6 +26,7 @@ import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
......@@ -37,9 +38,12 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for {@link BasicErrorController} using {@link MockMvc} but not
* {@link SpringJUnit4ClassRunner}.
*
* @author Dave Syer
*/
public class BasicErrorControllerSpecialIntegrationTests {
public class BasicErrorControllerDirectMockMvcTests {
private ConfigurableWebApplicationContext wac;
......
/*
* 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"));
}
}
/*
* Copyright 2012-2013 the original author or authors.
* 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.
......@@ -32,7 +32,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.web.BasicErrorControllerIntegrationTests.TestConfiguration;
import org.springframework.boot.autoconfigure.web.BasicErrorControllerMockMvcTests.TestConfiguration;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
......@@ -59,13 +59,16 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for {@link BasicErrorController} using {@link MockMvc} and
* {@link SpringJUnit4ClassRunner}.
*
* @author Dave Syer
*/
@SpringApplicationConfiguration(classes = TestConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@DirtiesContext
public class BasicErrorControllerIntegrationTests {
public class BasicErrorControllerMockMvcTests {
@Autowired
private WebApplicationContext wac;
......@@ -85,16 +88,6 @@ public class BasicErrorControllerIntegrationTests {
assertTrue("Wrong content: " + content, content.contains("999"));
}
@Test
public void testErrorForMachineClient() throws Exception {
MvcResult result = this.mockMvc.perform(get("/"))
.andExpect(status().is5xxServerError()).andReturn();
MvcResult response = this.mockMvc.perform(new ErrorDispatcher(result, "/error"))
.andReturn();
String content = response.getResponse().getContentAsString();
assertTrue("Wrong content: " + content, content.contains("Expected!"));
}
@Test
public void testErrorWithResponseStatus() throws Exception {
MvcResult result = this.mockMvc.perform(get("/bang"))
......
/*
* 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"));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment