From 70d4994502c848b3db82845c97a033448356c938 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Tue, 14 Apr 2020 09:29:54 -0500 Subject: [PATCH] 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 --- .../web/servlet/ManagementErrorEndpoint.java | 4 +- ...dContextConfigurationIntegrationTests.java | 4 +- .../AbstractWebEndpointIntegrationTests.java | 9 +- .../autoconfigure/web/ErrorProperties.java | 40 +++- .../AbstractErrorWebExceptionHandler.java | 39 +++- .../DefaultErrorWebExceptionHandler.java | 26 ++- .../error/AbstractErrorController.java | 29 ++- .../servlet/error/BasicErrorController.java | 26 ++- ...itional-spring-configuration-metadata.json | 4 + ...orWebExceptionHandlerIntegrationTests.java | 89 +++++++-- .../DefaultErrorWebExceptionHandlerTests.java | 5 +- .../BasicErrorControllerIntegrationTests.java | 180 +++++++++++++----- .../BasicErrorControllerMockMvcTests.java | 7 +- .../DefaultErrorViewIntegrationTests.java | 5 +- .../error/ErrorMvcAutoConfigurationTests.java | 8 +- ...DevToolsPropertyDefaultsPostProcessor.java | 1 + .../DevToolPropertiesIntegrationTests.java | 6 +- .../error/DefaultErrorAttributes.java | 25 ++- .../web/reactive/error/ErrorAttributes.java | 15 +- .../servlet/error/DefaultErrorAttributes.java | 70 +++++-- .../web/servlet/error/ErrorAttributes.java | 17 +- .../error/DefaultErrorAttributesTests.java | 61 ++++-- .../error/DefaultErrorAttributesTests.java | 117 ++++++++---- ...ctuatorCustomSecurityApplicationTests.java | 6 +- .../ui/SampleActuatorUiApplicationTests.java | 4 +- 25 files changed, 617 insertions(+), 180 deletions(-) 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 e5e0a7f21b8..0399ab5c99e 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 @@ -1,5 +1,5 @@ /* - * 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 invoke(ServletWebRequest request) { - return this.errorAttributes.getErrorAttributes(request, false); + return this.errorAttributes.getErrorAttributes(request, false, false); } } 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 c44059c796c..a255fc8e84f 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 @@ -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"); }); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java index dcef4df5b5b..9e334b2a83e 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * 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 the type of application context used by the tests * @author Andy Wilkinson + * @author Scott Frederick */ public abstract class AbstractWebEndpointIntegrationTests { @@ -409,8 +410,10 @@ public abstract class AbstractWebEndpointIntegrationTests consumer) { T applicationContext = this.applicationContextSupplier.get(); contextCustomizer.accept(applicationContext); - applicationContext.getEnvironment().getPropertySources() - .addLast(new MapPropertySource("test", Collections.singletonMap("endpointPath", endpointPath))); + Map 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)); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorProperties.java index d38a134f290..c7b0e889602 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorProperties.java @@ -1,5 +1,5 @@ /* - * 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 { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java index 7c9065f616f..3662f3da69a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java @@ -1,5 +1,5 @@ /* - * 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 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 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); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java index fdfc00e8add..951c87abac8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java @@ -1,5 +1,5 @@ /* - * 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 renderErrorView(ServerRequest request) { boolean includeStackTrace = isIncludeStackTrace(request, MediaType.TEXT_HTML); - Map error = getErrorAttributes(request, includeStackTrace); + boolean includeDetails = isIncludeDetails(request, MediaType.TEXT_HTML); + Map 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 renderErrorResponse(ServerRequest request) { boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL); - Map error = getErrorAttributes(request, includeStackTrace); + boolean includeDetails = isIncludeDetails(request, MediaType.ALL); + Map 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 diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/AbstractErrorController.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/AbstractErrorController.java index afb2f3dc4f8..f7b7140e24e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/AbstractErrorController.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/AbstractErrorController.java @@ -1,5 +1,5 @@ /* - * 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 getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) { + return this.getErrorAttributes(request, includeStackTrace, false); + } + + protected Map 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; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorController.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorController.java index 35cdc0ba4ab..2ddedb988c0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorController.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorController.java @@ -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 model = Collections - .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); + Map 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 body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); + Map 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 diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index e55c8324d6b..d1f92e89f81 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -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" diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerIntegrationTests.java index f3ec0171080..52dd04fcc4d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * 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("