diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java index e5541f150ba..d390b0f4f20 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java @@ -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 diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/TraceWebFilterAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/TraceWebFilterAutoConfiguration.java index 60cfe4326d9..1f821685a77 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/TraceWebFilterAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/TraceWebFilterAutoConfiguration.java @@ -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; } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ManagementErrorEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ManagementErrorEndpoint.java index f1cdd074400..4e4714d125a 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ManagementErrorEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ManagementErrorEndpoint.java @@ -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 invoke() { - RequestAttributes attributes = RequestContextHolder.currentRequestAttributes(); - return this.controller.extract(attributes, false, true); + return this.errorAttributes.getErrorAttributes( + RequestContextHolder.currentRequestAttributes(), false); } @Override diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/WebRequestTraceFilter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/WebRequestTraceFilter.java index bd11d6f661f..f64c07eac9f 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/WebRequestTraceFilter.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/WebRequestTraceFilter.java @@ -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 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; } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/WebRequestTraceFilterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/WebRequestTraceFilterTests.java index e592ff804fe..5901151134d 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/WebRequestTraceFilterTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/WebRequestTraceFilterTests.java @@ -1,5 +1,5 @@ /* - * 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); diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java index 96cace1a725..0b26cf06b4c 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java @@ -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) { diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/BasicErrorController.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/BasicErrorController.java index a4882d7a883..8cef0315301 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/BasicErrorController.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/BasicErrorController.java @@ -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 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> error(HttpServletRequest request) { - ServletRequestAttributes attributes = new ServletRequestAttributes(request); - String trace = request.getParameter("trace"); - Map extracted = extract(attributes, - trace != null && !"false".equals(trace.toLowerCase()), true); - HttpStatus statusCode = getStatus((Integer) extracted.get("status")); - return new ResponseEntity>(extracted, statusCode); + Map body = getErrorAttributes(request, getTraceParameter(request)); + HttpStatus status = getStatus(request); + return new ResponseEntity>(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 extract(RequestAttributes attributes, boolean trace, - boolean log) { - Map map = new LinkedHashMap(); - 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 getErrorAttributes(HttpServletRequest request, + boolean includeStackTrace) { + RequestAttributes requestAttributes = new ServletRequestAttributes(request); + return this.errorAttributes.getErrorAttributes(requestAttributes, + includeStackTrace); } - protected void addMessage(Map 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; } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/DefaulErrorAttributes.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/DefaulErrorAttributes.java new file mode 100644 index 00000000000..b6589a841e3 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/DefaulErrorAttributes.java @@ -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: + *
    + *
  • timestamp - The time that the errors were extracted
  • + *
  • status - The status code
  • + *
  • error - The error reason
  • + *
  • exception - The class name of the root exception
  • + *
  • message - The exception message
  • + *
  • errors - Any {@link ObjectError}s from a {@link BindingResult} exception + *
  • trace - The exception stack trace
  • + *
  • path - The URL path when the exception was raised
  • + *
+ * + * @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 getErrorAttributes(RequestAttributes requestAttributes, + boolean includeStackTrace) { + Map errorAttributes = new LinkedHashMap(); + errorAttributes.put("timestamp", new Date()); + addStatus(errorAttributes, requestAttributes); + addErrorDetails(errorAttributes, requestAttributes, includeStackTrace); + addPath(errorAttributes, requestAttributes); + return errorAttributes; + } + + private void addStatus(Map 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 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 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 errorAttributes, Throwable error) { + StringWriter stackTrace = new StringWriter(); + error.printStackTrace(new PrintWriter(stackTrace)); + stackTrace.flush(); + errorAttributes.put("trace", stackTrace.toString()); + } + + private void addPath(Map 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 getAttribute(RequestAttributes requestAttributes, String name) { + return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorAttributes.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorAttributes.java new file mode 100644 index 00000000000..b94e95ca77d --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorAttributes.java @@ -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 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); + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorController.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorController.java index 7041e6d8239..5611388a596 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorController.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorController.java @@ -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 extract(RequestAttributes attributes, boolean trace, - boolean log); - } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration.java index 722d43bdcdf..3fb34a5589a 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration.java @@ -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; + } } }; } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerSpecialIntegrationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerDirectMockMvcTests.java similarity index 93% rename from spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerSpecialIntegrationTests.java rename to spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerDirectMockMvcTests.java index 16603bd2f59..9556d1ef4ae 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerSpecialIntegrationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerDirectMockMvcTests.java @@ -1,5 +1,5 @@ /* - * 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; diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerIntegrationTest.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerIntegrationTest.java new file mode 100644 index 00000000000..55addff0ff2 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerIntegrationTest.java @@ -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 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 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")); + } +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerIntegrationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerMockMvcTests.java similarity index 92% rename from spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerIntegrationTests.java rename to spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerMockMvcTests.java index ab9fc2e1912..49925bc941f 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerIntegrationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerMockMvcTests.java @@ -1,5 +1,5 @@ /* - * 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")) diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/DefaultErrorAttributesTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/DefaultErrorAttributesTests.java new file mode 100644 index 00000000000..f6de308ed68 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/DefaultErrorAttributesTests.java @@ -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 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 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 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 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 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 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 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 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 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 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 attributes = this.errorAttributes.getErrorAttributes( + this.requestAttributes, false); + assertThat(attributes.get("path"), equalTo((Object) "path")); + + } +}