Commit 70d49945 authored by Scott Frederick's avatar Scott Frederick

Disable exception details on default error views

Prior to this commit, default error responses included the message
from a handled exception. When the exception was a BindException, the
error responses could also include an errors attribute containing the
details of the binding failure. These details could leak information
about the application.

This commit removes the exception message and binding errors detail
from error responses by default, and introduces a
`server.error.include-details` property that can be used to cause
these details to be included in the response.

Fixes gh-20505
parent 86614740
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
......@@ -47,7 +47,7 @@ public class ManagementErrorEndpoint {
@RequestMapping("${server.error.path:${error.path:/error}}")
@ResponseBody
public Map<String, Object> invoke(ServletWebRequest request) {
return this.errorAttributes.getErrorAttributes(request, false);
return this.errorAttributes.getErrorAttributes(request, false, false);
}
}
......@@ -41,6 +41,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Integration tests for {@link WebMvcEndpointChildContextConfiguration}.
*
* @author Phillip Webb
* @author Scott Frederick
*/
class WebMvcEndpointChildContextConfigurationIntegrationTests {
......@@ -59,7 +60,8 @@ class WebMvcEndpointChildContextConfigurationIntegrationTests {
WebClient client = WebClient.create("http://localhost:" + port);
ClientResponse response = client.get().uri("actuator/fail").accept(MediaType.APPLICATION_JSON)
.exchange().block();
assertThat(response.bodyToMono(String.class).block()).contains("message\":\"Epic Fail");
assertThat(response.bodyToMono(String.class).block())
.contains("message\":\"An error occurred while processing the request");
});
}
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
......@@ -61,6 +61,7 @@ import static org.mockito.Mockito.verify;
*
* @param <T> the type of application context used by the tests
* @author Andy Wilkinson
* @author Scott Frederick
*/
public abstract class AbstractWebEndpointIntegrationTests<T extends ConfigurableApplicationContext & AnnotationConfigRegistry> {
......@@ -409,8 +410,10 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
BiConsumer<ApplicationContext, WebTestClient> consumer) {
T applicationContext = this.applicationContextSupplier.get();
contextCustomizer.accept(applicationContext);
applicationContext.getEnvironment().getPropertySources()
.addLast(new MapPropertySource("test", Collections.singletonMap("endpointPath", endpointPath)));
Map<String, Object> properties = new HashMap<>();
properties.put("endpointPath", endpointPath);
properties.put("server.error.include-details", "always");
applicationContext.getEnvironment().getPropertySources().addLast(new MapPropertySource("test", properties));
applicationContext.refresh();
try {
InetSocketAddress address = new InetSocketAddress(getPort(applicationContext));
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
......@@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Value;
* @author Michael Stummvoll
* @author Stephane Nicoll
* @author Vedran Pavic
* @author Scott Frederick
* @since 1.3.0
*/
public class ErrorProperties {
......@@ -40,10 +41,15 @@ public class ErrorProperties {
private boolean includeException;
/**
* When to include a "stacktrace" attribute.
* When to include the "trace" attribute.
*/
private IncludeStacktrace includeStacktrace = IncludeStacktrace.NEVER;
/**
* When to include "message" and "errors" attributes.
*/
private IncludeDetails includeDetails = IncludeDetails.NEVER;
private final Whitelabel whitelabel = new Whitelabel();
public String getPath() {
......@@ -70,6 +76,14 @@ public class ErrorProperties {
this.includeStacktrace = includeStacktrace;
}
public IncludeDetails getIncludeDetails() {
return this.includeDetails;
}
public void setIncludeDetails(IncludeDetails includeDetails) {
this.includeDetails = includeDetails;
}
public Whitelabel getWhitelabel() {
return this.whitelabel;
}
......@@ -96,6 +110,28 @@ public class ErrorProperties {
}
/**
* Include error details attributes options.
*/
public enum IncludeDetails {
/**
* Never add error detail information.
*/
NEVER,
/**
* Always add error detail information.
*/
ALWAYS,
/**
* Add error details information when the "details" request parameter is "true".
*/
ON_DETAILS_PARAM
}
public static class Whitelabel {
/**
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
......@@ -54,6 +54,7 @@ import org.springframework.web.util.HtmlUtils;
* Abstract base class for {@link ErrorWebExceptionHandler} implementations.
*
* @author Brian Clozel
* @author Scott Frederick
* @since 2.0.0
* @see ErrorAttributes
*/
......@@ -131,10 +132,26 @@ public abstract class AbstractErrorWebExceptionHandler implements ErrorWebExcept
* views or JSON payloads.
* @param request the source request
* @param includeStackTrace whether to include the error stacktrace information
* @return the error attributes as a Map.
* @return the error attributes as a Map
* @deprecated since 2.3.0 in favor of
* {@link #getErrorAttributes(ServerRequest, boolean, boolean)}
*/
@Deprecated
protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
return this.errorAttributes.getErrorAttributes(request, includeStackTrace);
return this.errorAttributes.getErrorAttributes(request, includeStackTrace, false);
}
/**
* Extract the error attributes from the current request, to be used to populate error
* views or JSON payloads.
* @param request the source request
* @param includeStackTrace whether to include the error stacktrace information
* @param includeDetails whether to include message and errors attributes
* @return the error attributes as a Map
*/
protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace,
boolean includeDetails) {
return this.errorAttributes.getErrorAttributes(request, includeStackTrace, includeDetails);
}
/**
......@@ -152,7 +169,21 @@ public abstract class AbstractErrorWebExceptionHandler implements ErrorWebExcept
* @return {@code true} if the error trace has been requested, {@code false} otherwise
*/
protected boolean isTraceEnabled(ServerRequest request) {
String parameter = request.queryParam("trace").orElse("false");
return getBooleanParameter(request, "trace");
}
/**
* Check whether the details attribute has been set on the given request.
* @param request the source request
* @return {@code true} if the error details have been requested, {@code false}
* otherwise
*/
protected boolean isDetailsEnabled(ServerRequest request) {
return getBooleanParameter(request, "details");
}
private boolean getBooleanParameter(ServerRequest request, String parameterName) {
String parameter = request.queryParam(parameterName).orElse("false");
return !"false".equalsIgnoreCase(parameter);
}
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
......@@ -71,6 +71,7 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r
* payload.
*
* @author Brian Clozel
* @author Scott Frederick
* @since 2.0.0
*/
public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
......@@ -113,7 +114,8 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
*/
protected Mono<ServerResponse> renderErrorView(ServerRequest request) {
boolean includeStackTrace = isIncludeStackTrace(request, MediaType.TEXT_HTML);
Map<String, Object> error = getErrorAttributes(request, includeStackTrace);
boolean includeDetails = isIncludeDetails(request, MediaType.TEXT_HTML);
Map<String, Object> error = getErrorAttributes(request, includeStackTrace, includeDetails);
int errorStatus = getHttpStatus(error);
ServerResponse.BodyBuilder responseBody = ServerResponse.status(errorStatus).contentType(TEXT_HTML_UTF8);
return Flux.just(getData(errorStatus).toArray(new String[] {}))
......@@ -141,7 +143,8 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
*/
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL);
Map<String, Object> error = getErrorAttributes(request, includeStackTrace);
boolean includeDetails = isIncludeDetails(request, MediaType.ALL);
Map<String, Object> error = getErrorAttributes(request, includeStackTrace, includeDetails);
return ServerResponse.status(getHttpStatus(error)).contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(error));
}
......@@ -163,6 +166,23 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
return false;
}
/**
* Determine if the message and errors attributes should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the message and errors attributes should be included
*/
protected boolean isIncludeDetails(ServerRequest request, MediaType produces) {
ErrorProperties.IncludeDetails include = this.errorProperties.getIncludeDetails();
if (include == ErrorProperties.IncludeDetails.ALWAYS) {
return true;
}
if (include == ErrorProperties.IncludeDetails.ON_DETAILS_PARAM) {
return isDetailsEnabled(request);
}
return false;
}
/**
* Get the HTTP error status information from the error map.
* @param errorAttributes the current error information
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
......@@ -38,6 +38,7 @@ import org.springframework.web.servlet.ModelAndView;
*
* @author Dave Syer
* @author Phillip Webb
* @author Scott Frederick
* @since 1.3.0
* @see ErrorAttributes
*/
......@@ -66,13 +67,35 @@ public abstract class AbstractErrorController implements ErrorController {
return sorted;
}
/**
* Returns a {@link Map} of the error attributes.
* @param request the source request
* @param includeStackTrace if stack trace elements should be included
* @return the error attributes
* @deprecated since 2.3.0 in favor of
* {@link #getErrorAttributes(HttpServletRequest, boolean, boolean)}
*/
@Deprecated
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
return this.getErrorAttributes(request, includeStackTrace, false);
}
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace,
boolean includeDetails) {
WebRequest webRequest = new ServletWebRequest(request);
return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace, includeDetails);
}
protected boolean getTraceParameter(HttpServletRequest request) {
String parameter = request.getParameter("trace");
return getBooleanParameter(request, "trace");
}
protected boolean getDetailsParameter(HttpServletRequest request) {
return getBooleanParameter(request, "details");
}
protected boolean getBooleanParameter(HttpServletRequest request, String parameterName) {
String parameter = request.getParameter(parameterName);
if (parameter == null) {
return false;
}
......
......@@ -24,6 +24,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ErrorProperties.IncludeDetails;
import org.springframework.boot.autoconfigure.web.ErrorProperties.IncludeStacktrace;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
......@@ -47,6 +48,7 @@ import org.springframework.web.servlet.ModelAndView;
* @author Phillip Webb
* @author Michael Stummvoll
* @author Stephane Nicoll
* @author Scott Frederick
* @since 1.0.0
* @see ErrorAttributes
* @see ErrorProperties
......@@ -87,8 +89,8 @@ public class BasicErrorController extends AbstractErrorController {
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.TEXT_HTML), isIncludeDetails(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
......@@ -100,7 +102,8 @@ public class BasicErrorController extends AbstractErrorController {
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL),
isIncludeDetails(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
......@@ -127,6 +130,23 @@ public class BasicErrorController extends AbstractErrorController {
return false;
}
/**
* Determine if the error details attributes should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the error details attributes should be included
*/
protected boolean isIncludeDetails(HttpServletRequest request, MediaType produces) {
IncludeDetails include = getErrorProperties().getIncludeDetails();
if (include == IncludeDetails.ALWAYS) {
return true;
}
if (include == IncludeDetails.ON_DETAILS_PARAM) {
return getDetailsParameter(request);
}
return false;
}
/**
* Provide access to the error properties.
* @return the error properties
......
......@@ -106,6 +106,10 @@
"description": "Minimum \"Content-Length\" value that is required for compression to be performed.",
"defaultValue": "2KB"
},
{
"name": "server.error.include-details",
"defaultValue": "never"
},
{
"name": "server.error.include-stacktrace",
"defaultValue": "never"
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
......@@ -55,6 +55,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* Integration tests for {@link DefaultErrorWebExceptionHandler}
*
* @author Brian Clozel
* @author Scott Frederick
*/
@ExtendWith(OutputCaptureExtension.class)
class DefaultErrorWebExceptionHandlerIntegrationTests {
......@@ -78,8 +79,8 @@ class DefaultErrorWebExceptionHandlerIntegrationTests {
client.get().uri("/").exchange().expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectBody()
.jsonPath("status").isEqualTo("500").jsonPath("error")
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()).jsonPath("path").isEqualTo(("/"))
.jsonPath("message").isEqualTo("Expected!").jsonPath("exception").doesNotExist().jsonPath("trace")
.doesNotExist().jsonPath("requestId").isEqualTo(this.logIdFilter.getLogId());
.jsonPath("message").isEmpty().jsonPath("exception").doesNotExist().jsonPath("trace").doesNotExist()
.jsonPath("requestId").isEqualTo(this.logIdFilter.getLogId());
assertThat(output).contains("500 Server Error for HTTP GET \"/\"")
.contains("java.lang.IllegalStateException: Expected!");
});
......@@ -98,7 +99,7 @@ class DefaultErrorWebExceptionHandlerIntegrationTests {
@Test
void htmlError() {
this.contextRunner.run((context) -> {
this.contextRunner.withPropertyValues("server.error.include-details=always").run((context) -> {
WebTestClient client = getWebClient(context);
String body = client.get().uri("/").accept(MediaType.TEXT_HTML).exchange().expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectHeader().contentType(TEXT_HTML_UTF8)
......@@ -114,6 +115,18 @@ class DefaultErrorWebExceptionHandlerIntegrationTests {
client.post().uri("/bind").contentType(MediaType.APPLICATION_JSON).bodyValue("{}").exchange().expectStatus()
.isBadRequest().expectBody().jsonPath("status").isEqualTo("400").jsonPath("error")
.isEqualTo(HttpStatus.BAD_REQUEST.getReasonPhrase()).jsonPath("path").isEqualTo(("/bind"))
.jsonPath("exception").doesNotExist().jsonPath("errors").doesNotExist().jsonPath("message")
.isNotEmpty().jsonPath("requestId").isEqualTo(this.logIdFilter.getLogId());
});
}
@Test
void bindingResultErrorIncludeDetails() {
this.contextRunner.withPropertyValues("server.error.include-details=on-details-param").run((context) -> {
WebTestClient client = getWebClient(context);
client.post().uri("/bind?details=true").contentType(MediaType.APPLICATION_JSON).bodyValue("{}").exchange()
.expectStatus().isBadRequest().expectBody().jsonPath("status").isEqualTo("400").jsonPath("error")
.isEqualTo(HttpStatus.BAD_REQUEST.getReasonPhrase()).jsonPath("path").isEqualTo(("/bind"))
.jsonPath("exception").doesNotExist().jsonPath("errors").isArray().jsonPath("message").isNotEmpty()
.jsonPath("requestId").isEqualTo(this.logIdFilter.getLogId());
});
......@@ -163,6 +176,50 @@ class DefaultErrorWebExceptionHandlerIntegrationTests {
});
}
@Test
void includeDetailsOnParam() {
this.contextRunner.withPropertyValues("server.error.include-exception=true",
"server.error.include-details=on-details-param").run((context) -> {
WebTestClient client = getWebClient(context);
client.get().uri("/?details=true").exchange().expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectBody().jsonPath("status")
.isEqualTo("500").jsonPath("error")
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()).jsonPath("exception")
.isEqualTo(IllegalStateException.class.getName()).jsonPath("message").isNotEmpty()
.jsonPath("requestId").isEqualTo(this.logIdFilter.getLogId());
});
}
@Test
void alwaysIncludeDetails() {
this.contextRunner
.withPropertyValues("server.error.include-exception=true", "server.error.include-details=always")
.run((context) -> {
WebTestClient client = getWebClient(context);
client.get().uri("/?trace=false").exchange().expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectBody().jsonPath("status")
.isEqualTo("500").jsonPath("error")
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()).jsonPath("exception")
.isEqualTo(IllegalStateException.class.getName()).jsonPath("message").isNotEmpty()
.jsonPath("requestId").isEqualTo(this.logIdFilter.getLogId());
});
}
@Test
void neverIncludeDetails() {
this.contextRunner
.withPropertyValues("server.error.include-exception=true", "server.error.include-details=never")
.run((context) -> {
WebTestClient client = getWebClient(context);
client.get().uri("/?trace=true").exchange().expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectBody().jsonPath("status")
.isEqualTo("500").jsonPath("error")
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()).jsonPath("exception")
.isEqualTo(IllegalStateException.class.getName()).jsonPath("message").isEmpty()
.jsonPath("requestId").isEqualTo(this.logIdFilter.getLogId());
});
}
@Test
void statusException() {
this.contextRunner.withPropertyValues("server.error.include-exception=true").run((context) -> {
......@@ -176,8 +233,10 @@ class DefaultErrorWebExceptionHandlerIntegrationTests {
@Test
void defaultErrorView() {
this.contextRunner.withPropertyValues("spring.mustache.prefix=classpath:/unknown/",
"server.error.include-stacktrace=always").run((context) -> {
this.contextRunner
.withPropertyValues("spring.mustache.prefix=classpath:/unknown/",
"server.error.include-stacktrace=always", "server.error.include-details=always")
.run((context) -> {
WebTestClient client = getWebClient(context);
String body = client.get().uri("/").accept(MediaType.TEXT_HTML).exchange().expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectHeader().contentType(TEXT_HTML_UTF8)
......@@ -190,14 +249,16 @@ class DefaultErrorWebExceptionHandlerIntegrationTests {
@Test
void escapeHtmlInDefaultErrorView() {
this.contextRunner.withPropertyValues("spring.mustache.prefix=classpath:/unknown/").run((context) -> {
WebTestClient client = getWebClient(context);
String body = client.get().uri("/html").accept(MediaType.TEXT_HTML).exchange().expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectHeader().contentType(TEXT_HTML_UTF8)
.expectBody(String.class).returnResult().getResponseBody();
assertThat(body).contains("Whitelabel Error Page").contains(this.logIdFilter.getLogId())
.doesNotContain("<script>").contains("&lt;script&gt;");
});
this.contextRunner
.withPropertyValues("spring.mustache.prefix=classpath:/unknown/", "server.error.include-details=always")
.run((context) -> {
WebTestClient client = getWebClient(context);
String body = client.get().uri("/html").accept(MediaType.TEXT_HTML).exchange().expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectHeader().contentType(TEXT_HTML_UTF8)
.expectBody(String.class).returnResult().getResponseBody();
assertThat(body).contains("Whitelabel Error Page").contains(this.logIdFilter.getLogId())
.doesNotContain("<script>").contains("&lt;script&gt;");
});
}
@Test
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
......@@ -47,6 +47,7 @@ import static org.mockito.Mockito.mock;
*
* @author Phillip Webb
* @author Madhura Bhave
* @author Scott Frederick
*/
class DefaultErrorWebExceptionHandlerTests {
......@@ -65,7 +66,7 @@ class DefaultErrorWebExceptionHandlerTests {
ResourceProperties resourceProperties = new ResourceProperties();
ErrorProperties errorProperties = new ErrorProperties();
ApplicationContext context = new AnnotationConfigReactiveWebApplicationContext();
given(errorAttributes.getErrorAttributes(any(), anyBoolean())).willReturn(getErrorAttributes());
given(errorAttributes.getErrorAttributes(any(), anyBoolean(), anyBoolean())).willReturn(getErrorAttributes());
DefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler(errorAttributes,
resourceProperties, errorProperties, context);
setupViewResolver(exceptionHandler);
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
......@@ -67,8 +67,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* {@link SpringBootTest @SpringBootTest}.
*
* @author Dave Syer
* @author Scott Frederick
*/
@SpringBootTest
@SpringBootTest(properties = { "server.error.include-details=always" })
@DirtiesContext
class BasicErrorControllerMockMvcTests {
......@@ -117,7 +118,7 @@ class BasicErrorControllerMockMvcTests {
// And the rendered status code is always wrong (but would be 400 in a real
// system)
String content = response.getResponse().getContentAsString();
assertThat(content).contains("Error count: 1");
assertThat(content).contains("Validation failed");
}
@Test
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
......@@ -50,8 +50,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* Integration tests for the default error view.
*
* @author Dave Syer
* @author Scott Frederick
*/
@SpringBootTest
@SpringBootTest(properties = { "server.error.include-details=always" })
@DirtiesContext
class DefaultErrorViewIntegrationTests {
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
......@@ -45,13 +45,13 @@ class ErrorMvcAutoConfigurationTests {
AutoConfigurations.of(DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class));
@Test
void renderContainsViewWithExceptionDetails() throws Exception {
void renderContainsViewWithExceptionDetails() {
this.contextRunner.run((context) -> {
View errorView = context.getBean("error", View.class);
ErrorAttributes errorAttributes = context.getBean(ErrorAttributes.class);
DispatcherServletWebRequest webRequest = createWebRequest(new IllegalStateException("Exception message"),
false);
errorView.render(errorAttributes.getErrorAttributes(webRequest, true), webRequest.getRequest(),
errorView.render(errorAttributes.getErrorAttributes(webRequest, true, true), webRequest.getRequest(),
webRequest.getResponse());
assertThat(webRequest.getResponse().getContentType()).isEqualTo("text/html;charset=UTF-8");
String responseString = ((MockHttpServletResponse) webRequest.getResponse()).getContentAsString();
......@@ -69,7 +69,7 @@ class ErrorMvcAutoConfigurationTests {
ErrorAttributes errorAttributes = context.getBean(ErrorAttributes.class);
DispatcherServletWebRequest webRequest = createWebRequest(new IllegalStateException("Exception message"),
true);
errorView.render(errorAttributes.getErrorAttributes(webRequest, true), webRequest.getRequest(),
errorView.render(errorAttributes.getErrorAttributes(webRequest, true, true), webRequest.getRequest(),
webRequest.getResponse());
assertThat(output).contains("Cannot render error page for request [/path] "
+ "and exception [Exception message] as the response has "
......
......@@ -71,6 +71,7 @@ public class DevToolsPropertyDefaultsPostProcessor implements EnvironmentPostPro
properties.put("spring.resources.chain.cache", "false");
properties.put("spring.template.provider.cache", "false");
properties.put("spring.mvc.log-resolved-exception", "true");
properties.put("server.error.include-details", "ALWAYS");
properties.put("server.error.include-stacktrace", "ALWAYS");
properties.put("server.servlet.jsp.init-parameters.development", "true");
properties.put("spring.reactor.debug", "true");
......
......@@ -106,8 +106,10 @@ class DevToolPropertiesIntegrationTests {
application.setWebApplicationType(WebApplicationType.NONE);
this.context = getContext(application::run);
ConfigurableEnvironment environment = this.context.getEnvironment();
String property = environment.getProperty("server.error.include-stacktrace");
assertThat(property).isEqualTo(ErrorProperties.IncludeStacktrace.ALWAYS.toString());
String includeStackTrace = environment.getProperty("server.error.include-stacktrace");
assertThat(includeStackTrace).isEqualTo(ErrorProperties.IncludeStacktrace.ALWAYS.toString());
String includeDetails = environment.getProperty("server.error.include-details");
assertThat(includeDetails).isEqualTo(ErrorProperties.IncludeDetails.ALWAYS.toString());
}
protected ConfigurableApplicationContext getContext(Supplier<ConfigurableApplicationContext> supplier)
......
......@@ -53,6 +53,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Brian Clozel
* @author Stephane Nicoll
* @author Michele Mancioppi
* @author Scott Frederick
* @since 2.0.0
* @see ErrorAttributes
*/
......@@ -79,7 +80,14 @@ public class DefaultErrorAttributes implements ErrorAttributes {
}
@Override
@Deprecated
public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
return this.getErrorAttributes(request, includeStackTrace, false);
}
@Override
public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace,
boolean includeDetails) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
errorAttributes.put("path", request.path());
......@@ -89,9 +97,9 @@ public class DefaultErrorAttributes implements ErrorAttributes {
HttpStatus errorStatus = determineHttpStatus(error, responseStatusAnnotation);
errorAttributes.put("status", errorStatus.value());
errorAttributes.put("error", errorStatus.getReasonPhrase());
errorAttributes.put("message", determineMessage(error, responseStatusAnnotation));
errorAttributes.put("message", determineMessage(error, responseStatusAnnotation, includeDetails));
errorAttributes.put("requestId", request.exchange().getRequest().getId());
handleException(errorAttributes, determineException(error), includeStackTrace);
handleException(errorAttributes, determineException(error), includeStackTrace, includeDetails);
return errorAttributes;
}
......@@ -102,9 +110,13 @@ public class DefaultErrorAttributes implements ErrorAttributes {
return responseStatusAnnotation.getValue("code", HttpStatus.class).orElse(HttpStatus.INTERNAL_SERVER_ERROR);
}
private String determineMessage(Throwable error, MergedAnnotation<ResponseStatus> responseStatusAnnotation) {
private String determineMessage(Throwable error, MergedAnnotation<ResponseStatus> responseStatusAnnotation,
boolean includeDetails) {
if (error instanceof WebExchangeBindException) {
return error.getMessage();
return includeDetails ? error.getMessage() : "Validation failed";
}
if (!includeDetails) {
return "";
}
if (error instanceof ResponseStatusException) {
return ((ResponseStatusException) error).getReason();
......@@ -130,14 +142,15 @@ public class DefaultErrorAttributes implements ErrorAttributes {
errorAttributes.put("trace", stackTrace.toString());
}
private void handleException(Map<String, Object> errorAttributes, Throwable error, boolean includeStackTrace) {
private void handleException(Map<String, Object> errorAttributes, Throwable error, boolean includeStackTrace,
boolean includeDetails) {
if (this.includeException) {
errorAttributes.put("exception", error.getClass().getName());
}
if (includeStackTrace) {
addStackTrace(errorAttributes, error);
}
if (error instanceof BindingResult) {
if (includeDetails && (error instanceof BindingResult)) {
BindingResult result = (BindingResult) error;
if (result.hasErrors()) {
errorAttributes.put("errors", result.getAllErrors());
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.web.server.ServerWebExchange;
* Provides access to error attributes which can be logged or presented to the user.
*
* @author Brian Clozel
* @author Scott Frederick
* @since 2.0.0
* @see DefaultErrorAttributes
*/
......@@ -37,9 +38,21 @@ public interface ErrorAttributes {
* @param request the source request
* @param includeStackTrace if stack trace elements should be included
* @return a map of error attributes
* @deprecated since 2.3.0 in favor of
* {@link #getErrorAttributes(ServerRequest, boolean, boolean)}
*/
Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace);
/**
* Return a {@link Map} of the error attributes. The map can be used as the model of
* an error page, or returned as a {@link ServerResponse} body.
* @param request the source request
* @param includeStackTrace if stack trace elements should be included
* @param includeDetails if message and errors elements should be included
* @return a map of error attributes
*/
Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace, boolean includeDetails);
/**
* Return the underlying cause of the error or {@code null} if the error cannot be
* extracted.
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
......@@ -46,9 +46,10 @@ import org.springframework.web.servlet.ModelAndView;
* <li>status - The status code</li>
* <li>error - The error reason</li>
* <li>exception - The class name of the root exception (if configured)</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>message - The exception message (if configured)</li>
* <li>errors - Any {@link ObjectError}s from a {@link BindingResult} exception (if
* configured)</li>
* <li>trace - The exception stack trace (if configured)</li>
* <li>path - The URL path when the exception was raised</li>
* </ul>
*
......@@ -56,6 +57,7 @@ import org.springframework.web.servlet.ModelAndView;
* @author Dave Syer
* @author Stephane Nicoll
* @author Vedran Pavic
* @author Scott Frederick
* @since 2.0.0
* @see ErrorAttributes
*/
......@@ -99,11 +101,18 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
}
@Override
@Deprecated
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
return this.getErrorAttributes(webRequest, includeStackTrace, false);
}
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace,
boolean includeDetails) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addErrorDetails(errorAttributes, webRequest, includeStackTrace, includeDetails);
addPath(errorAttributes, webRequest);
return errorAttributes;
}
......@@ -125,8 +134,8 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
}
}
private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest webRequest,
boolean includeStackTrace) {
private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest webRequest, boolean includeStackTrace,
boolean includeDetails) {
Throwable error = getError(webRequest);
if (error != null) {
while (error instanceof ServletException && error.getCause() != null) {
......@@ -135,32 +144,51 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
if (this.includeException) {
errorAttributes.put("exception", error.getClass().getName());
}
addErrorMessage(errorAttributes, error);
if (includeStackTrace) {
addStackTrace(errorAttributes, error);
}
}
Object message = getAttribute(webRequest, "javax.servlet.error.message");
if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null)
&& !(error instanceof BindingResult)) {
errorAttributes.put("message", StringUtils.isEmpty(message) ? "No message available" : message);
}
addErrorMessage(errorAttributes, webRequest, error, includeDetails);
}
private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) {
private void addErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error,
boolean includeDetails) {
BindingResult result = extractBindingResult(error);
if (result == null) {
errorAttributes.put("message", error.getMessage());
addExceptionErrorMessage(errorAttributes, webRequest, error, includeDetails);
}
else {
addBindingResultErrorMessage(errorAttributes, result, includeDetails);
}
}
private void addExceptionErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error,
boolean includeDetails) {
if (!includeDetails) {
errorAttributes.put("message", "An error occurred while processing the request");
return;
}
Object message = getAttribute(webRequest, "javax.servlet.error.message");
if (StringUtils.isEmpty(message) && error != null) {
message = error.getMessage();
}
if (StringUtils.isEmpty(message)) {
message = "No message available";
}
errorAttributes.put("message", message);
}
private void addBindingResultErrorMessage(Map<String, Object> errorAttributes, BindingResult result,
boolean includeDetails) {
if (!includeDetails) {
errorAttributes.put("message", "Validation failed");
return;
}
if (result.hasErrors()) {
errorAttributes.put("errors", result.getAllErrors());
errorAttributes.put("message", "Validation failed for object='" + result.getObjectName()
+ "'. Error count: " + result.getErrorCount());
}
else {
errorAttributes.put("message", "No errors");
}
errorAttributes.put("message", "Validation failed for object='" + result.getObjectName() + "'. "
+ "Error count: " + result.getErrorCount());
}
private BindingResult extractBindingResult(Throwable error) {
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.web.servlet.ModelAndView;
* Provides access to error attributes which can be logged or presented to the user.
*
* @author Phillip Webb
* @author Scott Frederick
* @since 2.0.0
* @see DefaultErrorAttributes
*/
......@@ -38,9 +39,23 @@ public interface ErrorAttributes {
* @param webRequest the source request
* @param includeStackTrace if stack trace elements should be included
* @return a map of error attributes
* @deprecated since 2.3.0 in favor of
* {@link #getErrorAttributes(WebRequest, boolean, boolean)}
*/
@Deprecated
Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace);
/**
* 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 @ResponseBody}.
* @param webRequest the source request
* @param includeStackTrace if stack trace elements should be included
* @param includeDetails if message and errors elements should be included
* @return a map of error attributes
*/
Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace, boolean includeDetails);
/**
* Return the underlying cause of the error or {@code null} if the error cannot be
* extracted.
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
......@@ -34,8 +34,10 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Madhura Bhave
* @author Stephane Nicoll
* @author Scott Frederick
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = { "server.error.include-details=always" })
class SampleActuatorCustomSecurityApplicationTests extends AbstractSampleActuatorCustomSecurityTests {
@LocalServerPort
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
......@@ -39,7 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "server.error.include-details=always" })
class SampleActuatorUiApplicationTests {
@Autowired
......
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