Commit 4bdfb1af authored by Phillip Webb's avatar Phillip Webb

Merge pull request #2371 from Stummi/stacktraceproperties

* gh-2371:
  Support `error.include-stacktrace` configuration
parents f918bf9c a1e0f011
...@@ -43,6 +43,13 @@ public abstract class AbstractErrorController implements ErrorController { ...@@ -43,6 +43,13 @@ public abstract class AbstractErrorController implements ErrorController {
this.errorAttributes = errorAttributes; this.errorAttributes = errorAttributes;
} }
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
return this.errorAttributes.getErrorAttributes(requestAttributes,
includeStackTrace);
}
protected boolean getTraceParameter(HttpServletRequest request) { protected boolean getTraceParameter(HttpServletRequest request) {
String parameter = request.getParameter("trace"); String parameter = request.getParameter("trace");
if (parameter == null) { if (parameter == null) {
...@@ -51,17 +58,6 @@ public abstract class AbstractErrorController implements ErrorController { ...@@ -51,17 +58,6 @@ public abstract class AbstractErrorController implements ErrorController {
return !"false".equals(parameter.toLowerCase()); return !"false".equals(parameter.toLowerCase());
} }
protected Map<String, Object> getErrorAttributes(HttpServletRequest request) {
return getErrorAttributes(request, getTraceParameter(request));
}
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
return this.errorAttributes.getErrorAttributes(requestAttributes,
includeStackTrace);
}
protected HttpStatus getStatus(HttpServletRequest request) { protected HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code"); .getAttribute("javax.servlet.error.status_code");
......
...@@ -20,11 +20,13 @@ import java.util.Map; ...@@ -20,11 +20,13 @@ import java.util.Map;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.web.ErrorProperties.IncludeStacktrace;
import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
...@@ -37,35 +39,84 @@ import org.springframework.web.servlet.ModelAndView; ...@@ -37,35 +39,84 @@ import org.springframework.web.servlet.ModelAndView;
* *
* @author Dave Syer * @author Dave Syer
* @author Phillip Webb * @author Phillip Webb
* @author Michael Stummvoll
* @author Stephane Nicoll
* @see ErrorAttributes * @see ErrorAttributes
* @see ErrorProperties
*/ */
@Controller @Controller
@RequestMapping("${error.path:/error}") @RequestMapping("${error.path:/error}")
public class BasicErrorController extends AbstractErrorController { public class BasicErrorController extends AbstractErrorController {
@Value("${error.path:/error}") private final ErrorProperties errorProperties;
private String errorPath;
/**
* Create a new {@link BasicErrorController} instance.
* @param errorAttributes the error attributes
* @deprecated since 1.3.0 in favor of
* {@link #BasicErrorController(ErrorAttributes, ErrorProperties)}
*/
@Deprecated
public BasicErrorController(ErrorAttributes errorAttributes) { public BasicErrorController(ErrorAttributes errorAttributes) {
this(errorAttributes, new ErrorProperties());
}
/**
* Create a new {@link BasicErrorController} instance.
* @param errorAttributes the error attributes
* @param errorProperties configuration properties
*/
public BasicErrorController(ErrorAttributes errorAttributes,
ErrorProperties errorProperties) {
super(errorAttributes); super(errorAttributes);
Assert.notNull(errorProperties, "ErrorProperties must not be null");
this.errorProperties = errorProperties;
} }
@Override @Override
public String getErrorPath() { public String getErrorPath() {
return this.errorPath; return this.errorProperties.getPath();
} }
@RequestMapping(produces = "text/html") @RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request) { public ModelAndView errorHtml(HttpServletRequest request) {
return new ModelAndView("error", getErrorAttributes(request, false)); Map<String, Object> model = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.TEXT_HTML));
return new ModelAndView("error", model);
} }
@RequestMapping @RequestMapping
@ResponseBody @ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request); Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request); HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status); return new ResponseEntity<Map<String, Object>>(body, status);
} }
/**
* Determine if the stacktrace attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the stacktrace attribute should be included
*/
protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
IncludeStacktrace include = getErrorProperties().getIncludeStacktrace();
if (include == IncludeStacktrace.ALWAYS) {
return true;
}
if (include == IncludeStacktrace.ON_TRACE_PARAM) {
return getTraceParameter(request);
}
return false;
}
/**
* Provide access to the error properties.
* @return the error properties
*/
protected final ErrorProperties getErrorProperties() {
return this.errorProperties;
}
} }
...@@ -25,7 +25,6 @@ import javax.servlet.http.HttpServletRequest; ...@@ -25,7 +25,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
...@@ -39,6 +38,7 @@ import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvi ...@@ -39,6 +38,7 @@ import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvi
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.ErrorPage; import org.springframework.boot.context.embedded.ErrorPage;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Conditional;
...@@ -63,18 +63,20 @@ import org.springframework.web.util.HtmlUtils; ...@@ -63,18 +63,20 @@ import org.springframework.web.util.HtmlUtils;
* *
* @author Dave Syer * @author Dave Syer
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Stephane Nicoll
*/ */
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@ConditionalOnWebApplication @ConditionalOnWebApplication
// Ensure this loads before the main WebMvcAutoConfiguration so that the error View is // Ensure this loads before the main WebMvcAutoConfiguration so that the error View is
// available // available
@AutoConfigureBefore(WebMvcAutoConfiguration.class) @AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(ErrorProperties.class)
@Configuration @Configuration
public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustomizer, public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustomizer,
Ordered { Ordered {
@Value("${error.path:/error}") @Autowired
private String errorPath = "/error"; private ErrorProperties errorProperties;
@Autowired @Autowired
private ServerProperties properties; private ServerProperties properties;
...@@ -93,13 +95,13 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom ...@@ -93,13 +95,13 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom
@Bean @Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) { public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes); return new BasicErrorController(errorAttributes, this.errorProperties);
} }
@Override @Override
public void customize(ConfigurableEmbeddedServletContainer container) { public void customize(ConfigurableEmbeddedServletContainer container) {
container.addErrorPages(new ErrorPage(this.properties.getServletPrefix() container.addErrorPages(new ErrorPage(this.properties.getServletPrefix()
+ this.errorPath)); + this.errorProperties.getPath()));
} }
@Configuration @Configuration
......
/*
* Copyright 2012-2015 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 org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for web error handling.
*
* @author Michael Stummvoll
* @author Stephane Nicoll
* @since 1.3.0
*/
@ConfigurationProperties("error")
public class ErrorProperties {
/**
* Path of the error controller.
*/
private String path = "/error";
/**
* When to include a "stacktrace" attribute.
*/
private IncludeStacktrace includeStacktrace = IncludeStacktrace.NEVER;
public String getPath() {
return this.path;
}
public void setPath(String path) {
this.path = path;
}
public IncludeStacktrace getIncludeStacktrace() {
return this.includeStacktrace;
}
public void setIncludeStacktrace(IncludeStacktrace includeStacktrace) {
this.includeStacktrace = includeStacktrace;
}
/**
* Include Stacktrace attribute options.
*/
public enum IncludeStacktrace {
/**
* Never add stacktrace information.
*/
NEVER,
/**
* Always add stacktrace information.
*/
ALWAYS,
/**
* Add stacktrace information when the "trace" request parameter is "true".
*/
ON_TRACE_PARAM
}
}
/* /*
* Copyright 2012-2014 the original author or authors. * Copyright 2012-2015 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -17,20 +17,24 @@ ...@@ -17,20 +17,24 @@
package org.springframework.boot.autoconfigure.web; package org.springframework.boot.autoconfigure.web;
import java.net.URI; import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map; import java.util.Map;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.web.BasicErrorControllerIntegrationTests.TestConfiguration; import org.springframework.boot.autoconfigure.web.BasicErrorControllerIntegrationTests.TestConfiguration;
import org.springframework.boot.autoconfigure.web.BasicErrorControllerMockMvcTests.MinimalWebConfiguration; import org.springframework.boot.autoconfigure.web.BasicErrorControllerMockMvcTests.MinimalWebConfiguration;
import org.springframework.boot.test.IntegrationTest; import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate; import org.springframework.boot.test.TestRestTemplate;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
...@@ -48,15 +52,17 @@ import org.springframework.web.servlet.View; ...@@ -48,15 +52,17 @@ import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.AbstractView; import org.springframework.web.servlet.view.AbstractView;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/** /**
* Tests for {@link BasicErrorController} using {@link IntegrationTest} that hit a real * Tests for {@link BasicErrorController} using a real HTTP server.
* HTTP server.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Dave Syer * @author Dave Syer
* @author Stephane Nicoll
*/ */
@RunWith(SpringJUnit4ClassRunner.class) @RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(TestConfiguration.class) @SpringApplicationConfiguration(TestConfiguration.class)
...@@ -65,50 +71,87 @@ import static org.junit.Assert.assertThat; ...@@ -65,50 +71,87 @@ import static org.junit.Assert.assertThat;
@IntegrationTest("server.port=0") @IntegrationTest("server.port=0")
public class BasicErrorControllerIntegrationTests { public class BasicErrorControllerIntegrationTests {
@Value("${local.server.port}") private ConfigurableApplicationContext context;
private int port;
@After
public void closeContext() {
if (this.context != null) {
this.context.close();
}
}
@Test @Test
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
public void testErrorForMachineClient() throws Exception { public void testErrorForMachineClient() throws Exception {
load();
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(
createUrl("?trace=true"), Map.class);
assertErrorAttributes(entity.getBody(), "500", "" + "Internal Server Error",
IllegalStateException.class, "Expected!", "/");
assertFalse("trace parameter should not be set",
entity.getBody().containsKey("trace"));
}
@Test
@SuppressWarnings("rawtypes")
public void testErrorForMachineClientTracePramamStacktrace() throws Exception {
load("--error.include-stacktrace=on-trace-param");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(
createUrl("?trace=true"), Map.class);
assertErrorAttributes(entity.getBody(), "500", "" + "Internal Server Error",
IllegalStateException.class, "Expected!", "/");
assertTrue("trace parameter should be set", entity.getBody().containsKey("trace"));
}
@Test
@SuppressWarnings("rawtypes")
public void testErrorForMachineClientNoStacktrace() throws Exception {
load("--error.include-stacktrace=never");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity( ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(
"http://localhost:" + this.port, Map.class); createUrl("?trace=true"), Map.class);
String body = entity.getBody().toString(); assertErrorAttributes(entity.getBody(), "500", "" + "Internal Server Error",
assertThat(body, endsWith("status=500, " + "error=Internal Server Error, " IllegalStateException.class, "Expected!", "/");
+ "exception=java.lang.IllegalStateException, " + "message=Expected!, " assertFalse("trace parameter should not be set",
+ "path=/}")); entity.getBody().containsKey("trace"));
}
@Test
@SuppressWarnings("rawtypes")
public void testErrorForMachineClientAlwaysStacktrace() throws Exception {
load("--error.include-stacktrace=always");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(
createUrl("?trace=false"), Map.class);
assertErrorAttributes(entity.getBody(), "500", "" + "Internal Server Error",
IllegalStateException.class, "Expected!", "/");
assertTrue("trace parameter should be set", entity.getBody().containsKey("trace"));
} }
@Test @Test
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
public void testErrorForAnnotatedException() throws Exception { public void testErrorForAnnotatedException() throws Exception {
load();
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity( ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(
"http://localhost:" + this.port + "/annotated", Map.class); createUrl("/annotated"), Map.class);
assertThat( assertErrorAttributes(entity.getBody(), "400", "Bad Request",
entity.getBody().toString(), TestConfiguration.Errors.ExpectedException.class, "Expected!",
endsWith("status=400, " "/annotated");
+ "error=Bad Request, "
+ "exception=org.springframework.boot.autoconfigure.web.BasicErrorControllerIntegrationTests$TestConfiguration$Errors$ExpectedException, "
+ "message=Expected!, " + "path=/annotated}"));
} }
@Test @Test
public void testErrorForAnnotatedNoReasonException() throws Exception { public void testErrorForAnnotatedNoReasonException() throws Exception {
ResponseEntity<?> entity = new TestRestTemplate().getForEntity( load();
"http://localhost:" + this.port + "/annotatedNoReason", Object.class); ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(
assertThat( createUrl("/annotatedNoReason"), Map.class);
entity.getBody().toString(), assertErrorAttributes(entity.getBody(), "406", "Not Acceptable",
endsWith("status=406, " TestConfiguration.Errors.NoReasonExpectedException.class,
+ "error=Not Acceptable, " "Expected message", "/annotatedNoReason");
+ "exception=org.springframework.boot.autoconfigure.web.BasicErrorControllerIntegrationTests$TestConfiguration$Errors$NoReasonExpectedException, "
+ "message=Expected message, " + "path=/annotatedNoReason}"));
} }
@Test @Test
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
public void testBindingExceptionForMachineClient() throws Exception { public void testBindingExceptionForMachineClient() throws Exception {
RequestEntity request = RequestEntity load();
.get(URI.create("http://localhost:" + this.port + "/bind")) RequestEntity request = RequestEntity.get(URI.create(createUrl("/bind")))
.accept(MediaType.APPLICATION_JSON).build(); .accept(MediaType.APPLICATION_JSON).build();
ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class); ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class);
String resp = entity.getBody().toString(); String resp = entity.getBody().toString();
...@@ -118,6 +161,31 @@ public class BasicErrorControllerIntegrationTests { ...@@ -118,6 +161,31 @@ public class BasicErrorControllerIntegrationTests {
assertThat(resp, containsString("org.springframework.validation.BindException")); assertThat(resp, containsString("org.springframework.validation.BindException"));
} }
private void assertErrorAttributes(Map<?, ?> content, String status, String error,
Class<?> exception, String message, String path) {
assertEquals("Wrong status", status, content.get("status"));
assertEquals("Wrong error", error, content.get("error"));
assertEquals("Wrong exception", exception.getName(), content.get("exception"));
assertEquals("Wrong message", message, content.get("message"));
assertEquals("Wrong path", path, content.get("path"));
}
private String createUrl(String path) {
int port = this.context.getEnvironment().getProperty("local.server.port",
int.class);
return "http://localhost:" + port + path;
}
private void load(String... arguments) {
List<String> args = new ArrayList<String>();
args.add("--server.port=0");
if (arguments != null) {
args.addAll(Arrays.asList(arguments));
}
this.context = SpringApplication.run(TestConfiguration.class,
args.toArray(new String[args.size()]));
}
@Configuration @Configuration
@MinimalWebConfiguration @MinimalWebConfiguration
public static class TestConfiguration { public static class TestConfiguration {
...@@ -162,7 +230,7 @@ public class BasicErrorControllerIntegrationTests { ...@@ -162,7 +230,7 @@ public class BasicErrorControllerIntegrationTests {
} }
@RequestMapping("/bind") @RequestMapping("/bind")
public String bind(HttpServletRequest request) throws Exception { public String bind() throws Exception {
BindException error = new BindException(this, "test"); BindException error = new BindException(this, "test");
error.rejectValue("foo", "bar.error"); error.rejectValue("foo", "bar.error");
throw error; throw error;
......
...@@ -136,6 +136,10 @@ content into your application; rather pick only the properties that you need. ...@@ -136,6 +136,10 @@ content into your application; rather pick only the properties that you need.
server.undertow.io-threads= # number of I/O threads to create for the worker server.undertow.io-threads= # number of I/O threads to create for the worker
server.undertow.worker-threads= # number of worker threads server.undertow.worker-threads= # number of worker threads
# ERROR HANDLING ({sc-spring-boot-autoconfigure}/web/ErrorProperties.{sc-ext}[ErrorProperties])
error.path=/error # the error path
error.include-stacktrace=never # when to include a stacktrace attribute (never/alway/on-trace-param)
# SPRING MVC ({sc-spring-boot-autoconfigure}/web/WebMvcProperties.{sc-ext}[WebMvcProperties]) # SPRING MVC ({sc-spring-boot-autoconfigure}/web/WebMvcProperties.{sc-ext}[WebMvcProperties])
spring.mvc.locale= # set fixed locale, e.g. en_UK spring.mvc.locale= # set fixed locale, e.g. en_UK
spring.mvc.date-format= # set fixed date format, e.g. dd/MM/yyyy spring.mvc.date-format= # set fixed date format, e.g. dd/MM/yyyy
......
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