diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpoint.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpoint.java index 0399ab5c99..4fd070eb4b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpoint.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpoint.java @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.web.servlet; import java.util.Map; +import org.springframework.boot.autoconfigure.web.ErrorProperties; import org.springframework.boot.web.servlet.error.ErrorAttributes; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.stereotype.Controller; @@ -32,6 +33,7 @@ import org.springframework.web.context.request.ServletWebRequest; * but because of the way the handler mappings are set up it will not be detected. * * @author Dave Syer + * @author Scott Frederick * @since 2.0.0 */ @Controller @@ -39,15 +41,49 @@ public class ManagementErrorEndpoint { private final ErrorAttributes errorAttributes; - public ManagementErrorEndpoint(ErrorAttributes errorAttributes) { + private final ErrorProperties errorProperties; + + public ManagementErrorEndpoint(ErrorAttributes errorAttributes, ErrorProperties errorProperties) { Assert.notNull(errorAttributes, "ErrorAttributes must not be null"); + Assert.notNull(errorProperties, "ErrorProperties must not be null"); this.errorAttributes = errorAttributes; + this.errorProperties = errorProperties; } @RequestMapping("${server.error.path:${error.path:/error}}") @ResponseBody public Map invoke(ServletWebRequest request) { - return this.errorAttributes.getErrorAttributes(request, false, false); + return this.errorAttributes.getErrorAttributes(request, includeStackTrace(request), includeDetails(request)); + } + + private boolean includeStackTrace(ServletWebRequest request) { + ErrorProperties.IncludeStacktrace include = this.errorProperties.getIncludeStacktrace(); + if (include == ErrorProperties.IncludeStacktrace.ALWAYS) { + return true; + } + if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM) { + return getBooleanParameter(request, "trace"); + } + return false; + } + + private boolean includeDetails(ServletWebRequest request) { + ErrorProperties.IncludeDetails include = this.errorProperties.getIncludeDetails(); + if (include == ErrorProperties.IncludeDetails.ALWAYS) { + return true; + } + if (include == ErrorProperties.IncludeDetails.ON_DETAILS_PARAM) { + return getBooleanParameter(request, "details"); + } + return false; + } + + protected boolean getBooleanParameter(ServletWebRequest request, String parameterName) { + String parameter = request.getParameter(parameterName); + if (parameter == null) { + return false; + } + return !"false".equalsIgnoreCase(parameter); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java index 97974af4aa..c5b48975f5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java @@ -61,8 +61,8 @@ class WebMvcEndpointChildContextConfiguration { */ @Bean @ConditionalOnBean(ErrorAttributes.class) - ManagementErrorEndpoint errorEndpoint(ErrorAttributes errorAttributes) { - return new ManagementErrorEndpoint(errorAttributes); + ManagementErrorEndpoint errorEndpoint(ErrorAttributes errorAttributes, ServerProperties serverProperties) { + return new ManagementErrorEndpoint(errorAttributes, serverProperties.getError()); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpointTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpointTests.java new file mode 100644 index 0000000000..7c4362626a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpointTests.java @@ -0,0 +1,106 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://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.actuate.autoconfigure.web.servlet; + +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.web.ErrorProperties; +import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.ServletWebRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ManagementErrorEndpoint}. + * + * @author Scott Frederick + */ +class ManagementErrorEndpointTests { + + private final ErrorAttributes errorAttributes = new DefaultErrorAttributes(); + + private final ErrorProperties errorProperties = new ErrorProperties(); + + private final MockHttpServletRequest request = new MockHttpServletRequest(); + + @BeforeEach + void setUp() { + this.request.setAttribute("javax.servlet.error.exception", new RuntimeException("test exception")); + } + + @Test + void errorResponseNeverDetails() { + ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties); + Map response = endpoint.invoke(new ServletWebRequest(new MockHttpServletRequest())); + assertThat(response).containsEntry("message", "An error occurred while processing the request"); + assertThat(response).doesNotContainKey("trace"); + } + + @Test + void errorResponseAlwaysDetails() { + this.errorProperties.setIncludeStacktrace(ErrorProperties.IncludeStacktrace.ALWAYS); + this.errorProperties.setIncludeDetails(ErrorProperties.IncludeDetails.ALWAYS); + this.request.addParameter("trace", "false"); + this.request.addParameter("details", "false"); + ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties); + Map response = endpoint.invoke(new ServletWebRequest(this.request)); + assertThat(response).containsEntry("message", "test exception"); + assertThat(response).hasEntrySatisfying("trace", + (value) -> assertThat(value).asString().startsWith("java.lang.RuntimeException: test exception")); + } + + @Test + void errorResponseParamsAbsent() { + this.errorProperties.setIncludeStacktrace(ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM); + this.errorProperties.setIncludeDetails(ErrorProperties.IncludeDetails.ON_DETAILS_PARAM); + ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties); + Map response = endpoint.invoke(new ServletWebRequest(this.request)); + assertThat(response).containsEntry("message", "An error occurred while processing the request"); + assertThat(response).doesNotContainKey("trace"); + } + + @Test + void errorResponseParamsTrue() { + this.errorProperties.setIncludeStacktrace(ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM); + this.errorProperties.setIncludeDetails(ErrorProperties.IncludeDetails.ON_DETAILS_PARAM); + this.request.addParameter("trace", "true"); + this.request.addParameter("details", "true"); + ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties); + Map response = endpoint.invoke(new ServletWebRequest(this.request)); + assertThat(response).containsEntry("message", "test exception"); + assertThat(response).hasEntrySatisfying("trace", + (value) -> assertThat(value).asString().startsWith("java.lang.RuntimeException: test exception")); + } + + @Test + void errorResponseParamsFalse() { + this.errorProperties.setIncludeStacktrace(ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM); + this.errorProperties.setIncludeDetails(ErrorProperties.IncludeDetails.ON_DETAILS_PARAM); + this.request.addParameter("trace", "false"); + this.request.addParameter("details", "false"); + ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties); + Map response = endpoint.invoke(new ServletWebRequest(this.request)); + assertThat(response).containsEntry("message", "An error occurred while processing the request"); + assertThat(response).doesNotContainKey("trace"); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java index a255fc8e84..f3630545f9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.web.servlet; +import java.util.Map; + import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; @@ -45,23 +47,43 @@ import static org.assertj.core.api.Assertions.assertThat; */ class WebMvcEndpointChildContextConfigurationIntegrationTests { + private final WebApplicationContextRunner runner = new WebApplicationContextRunner( + AnnotationConfigServletWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class, + ServletWebServerFactoryAutoConfiguration.class, + ServletManagementContextAutoConfiguration.class, WebEndpointAutoConfiguration.class, + EndpointAutoConfiguration.class, DispatcherServletAutoConfiguration.class, + ErrorMvcAutoConfiguration.class)) + .withUserConfiguration(FailingEndpoint.class) + .withInitializer(new ServerPortInfoApplicationContextInitializer()).withPropertyValues( + "server.port=0", "management.server.port=0", "management.endpoints.web.exposure.include=*"); + @Test // gh-17938 + @SuppressWarnings("unchecked") void errorPageAndErrorControllerAreUsed() { - new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) - .withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class, - ServletWebServerFactoryAutoConfiguration.class, ServletManagementContextAutoConfiguration.class, - WebEndpointAutoConfiguration.class, EndpointAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class)) - .withUserConfiguration(FailingEndpoint.class) - .withInitializer(new ServerPortInfoApplicationContextInitializer()).withPropertyValues("server.port=0", - "management.server.port=0", "management.endpoints.web.exposure.include=*") + this.runner.run((context) -> { + String port = context.getEnvironment().getProperty("local.management.port"); + WebClient client = WebClient.create("http://localhost:" + port); + ClientResponse response = client.get().uri("actuator/fail").accept(MediaType.APPLICATION_JSON).exchange() + .block(); + Map body = response.bodyToMono(Map.class).block(); + assertThat(body).containsEntry("message", "An error occurred while processing the request"); + assertThat(body).doesNotContainKey("trace"); + }); + } + + @Test + void errorPageAndErrorControllerIncludeDetails() { + this.runner.withPropertyValues("server.error.include-stacktrace=always", "server.error.include-details=always") .run((context) -> { String port = context.getEnvironment().getProperty("local.management.port"); 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\":\"An error occurred while processing the request"); + Map body = response.bodyToMono(Map.class).block(); + assertThat(body).containsEntry("message", "Epic Fail"); + assertThat(body).hasEntrySatisfying("trace", (value) -> assertThat(value).asString() + .contains("java.lang.IllegalStateException: Epic Fail")); }); }