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 e5e0a7f21b..0399ab5c99 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 c44059c796..a255fc8e84 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 dcef4df5b5..9e334b2a83 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 d38a134f29..c7b0e88960 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 7c9065f616..3662f3da69 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 fdfc00e8ad..951c87abac 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 afb2f3dc4f..f7b7140e24 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 35cdc0ba4a..2ddedb988c 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 e55c8324d6..d1f92e89f8 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 f3ec017108..52dd04fcc4 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("