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 2ad1eb96b1..b79c3219bc 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 @@ -189,8 +189,9 @@ public abstract class AbstractErrorWebExceptionHandler protected Mono renderDefaultErrorView( ServerResponse.BodyBuilder responseBody, Map error) { StringBuilder builder = new StringBuilder(); - Object message = error.get("message"); Date timestamp = (Date) error.get("timestamp"); + Object message = error.get("message"); + Object trace = error.get("trace"); builder.append("

Whitelabel Error Page

").append( "

This application has no configured error view, so you are seeing this as a fallback.

") .append("
").append(timestamp).append("
") @@ -200,6 +201,9 @@ public abstract class AbstractErrorWebExceptionHandler if (message != null) { builder.append("
").append(htmlEscape(message)).append("
"); } + if (trace != null) { + builder.append("
").append(htmlEscape(trace)).append("
"); + } builder.append(""); return responseBody.syncBody(builder.toString()); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.java index d1f6a8a527..ab76336bad 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.java @@ -16,8 +16,7 @@ package org.springframework.boot.autoconfigure.web.servlet.error; -import java.util.Collections; -import java.util.HashMap; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -66,14 +65,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.context.expression.MapAccessor; import org.springframework.core.Ordered; import org.springframework.core.type.AnnotatedTypeMetadata; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.SimpleEvaluationContext; -import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.View; import org.springframework.web.servlet.view.BeanNameViewResolver; @@ -86,6 +79,7 @@ import org.springframework.web.util.HtmlUtils; * @author Dave Syer * @author Andy Wilkinson * @author Stephane Nicoll + * @author Brian Clozel */ @Configuration @ConditionalOnWebApplication(type = Type.SERVLET) @@ -163,12 +157,7 @@ public class ErrorMvcAutoConfiguration { @Conditional(ErrorTemplateMissingCondition.class) protected static class WhitelabelErrorViewConfiguration { - private final SpelView defaultErrorView = new SpelView( - "

Whitelabel Error Page

" - + "

This application has no explicit mapping for /error, so you are seeing this as a fallback.

" - + "
${timestamp}
" - + "
There was an unexpected error (type=${error}, status=${status}).
" - + "
${message}
"); + private final StaticView defaultErrorView = new StaticView(); @Bean(name = "error") @ConditionalOnMissingBean(name = "error") @@ -214,27 +203,11 @@ public class ErrorMvcAutoConfiguration { } /** - * Simple {@link View} implementation that resolves variables as SpEL expressions. + * Simple {@link View} implementation that writes a default HTML error page. */ - private static class SpelView implements View { + private static class StaticView implements View { - private static final Log logger = LogFactory.getLog(SpelView.class); - - private final NonRecursivePropertyPlaceholderHelper helper; - - private final String template; - - private volatile Map expressions; - - SpelView(String template) { - this.helper = new NonRecursivePropertyPlaceholderHelper("${", "}"); - this.template = template; - } - - @Override - public String getContentType() { - return "text/html"; - } + private static final Log logger = LogFactory.getLog(StaticView.class); @Override public void render(Map model, HttpServletRequest request, @@ -244,13 +217,31 @@ public class ErrorMvcAutoConfiguration { logger.error(message); return; } + StringBuilder builder = new StringBuilder(); + Date timestamp = (Date) model.get("timestamp"); + Object message = model.get("message"); + Object trace = model.get("trace"); if (response.getContentType() == null) { response.setContentType(getContentType()); } - PlaceholderResolver resolver = new ExpressionResolver(getExpressions(), - model); - String result = this.helper.replacePlaceholders(this.template, resolver); - response.getWriter().append(result); + builder.append("

Whitelabel Error Page

").append( + "

This application has no configured error view, so you are seeing this as a fallback.

") + .append("
").append(timestamp).append("
") + .append("
There was an unexpected error (type=") + .append(htmlEscape(model.get("error"))).append(", status=") + .append(htmlEscape(model.get("status"))).append(").
"); + if (message != null) { + builder.append("
").append(htmlEscape(message)).append("
"); + } + if (trace != null) { + builder.append("
").append(htmlEscape(trace)).append("
"); + } + builder.append(""); + response.getWriter().append(builder.toString()); + } + + private String htmlEscape(Object input) { + return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null; } private String getMessage(Map model) { @@ -264,69 +255,9 @@ public class ErrorMvcAutoConfiguration { return message; } - private Map getExpressions() { - if (this.expressions == null) { - synchronized (this) { - ExpressionCollector expressionCollector = new ExpressionCollector(); - this.helper.replacePlaceholders(this.template, expressionCollector); - this.expressions = expressionCollector.getExpressions(); - } - } - return this.expressions; - } - - } - - /** - * {@link PlaceholderResolver} to collect placeholder expressions. - */ - private static class ExpressionCollector implements PlaceholderResolver { - - private final SpelExpressionParser parser = new SpelExpressionParser(); - - private final Map expressions = new HashMap<>(); - - @Override - public String resolvePlaceholder(String name) { - this.expressions.put(name, this.parser.parseExpression(name)); - return null; - } - - public Map getExpressions() { - return Collections.unmodifiableMap(this.expressions); - } - - } - - /** - * SpEL based {@link PlaceholderResolver}. - */ - private static class ExpressionResolver implements PlaceholderResolver { - - private final Map expressions; - - private final EvaluationContext context; - - ExpressionResolver(Map expressions, Map map) { - this.expressions = expressions; - this.context = getContext(map); - } - - private EvaluationContext getContext(Map map) { - return SimpleEvaluationContext.forPropertyAccessors(new MapAccessor()) - .withRootObject(map).build(); - } - @Override - public String resolvePlaceholder(String placeholderName) { - Expression expression = this.expressions.get(placeholderName); - Object expressionValue = (expression != null) - ? expression.getValue(this.context) : null; - return escape(expressionValue); - } - - private String escape(Object value) { - return HtmlUtils.htmlEscape((value != null) ? value.toString() : null); + public String getContentType() { + return "text/html"; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/NonRecursivePropertyPlaceholderHelper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/NonRecursivePropertyPlaceholderHelper.java deleted file mode 100644 index 607916f06c..0000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/NonRecursivePropertyPlaceholderHelper.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2012-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.web.servlet.error; - -import java.util.Set; - -import org.springframework.util.PropertyPlaceholderHelper; - -/** - * {@link PropertyPlaceholderHelper} that doesn't allow recursive resolution. - * - * @author Phillip Webb - */ -class NonRecursivePropertyPlaceholderHelper extends PropertyPlaceholderHelper { - - NonRecursivePropertyPlaceholderHelper(String placeholderPrefix, - String placeholderSuffix) { - super(placeholderPrefix, placeholderSuffix); - } - - @Override - protected String parseStringValue(String strVal, - PlaceholderResolver placeholderResolver, Set visitedPlaceholders) { - return super.parseStringValue(strVal, - new NonRecursivePlaceholderResolver(placeholderResolver), - visitedPlaceholders); - } - - private static class NonRecursivePlaceholderResolver implements PlaceholderResolver { - - private final PlaceholderResolver resolver; - - NonRecursivePlaceholderResolver(PlaceholderResolver resolver) { - this.resolver = resolver; - } - - @Override - public String resolvePlaceholder(String placeholderName) { - if (this.resolver instanceof NonRecursivePlaceholderResolver) { - return null; - } - return this.resolver.resolvePlaceholder(placeholderName); - } - - } - -} 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 b289338aa5..c9e30b0d0b 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 @@ -184,7 +184,8 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests { @Test public void defaultErrorView() { this.contextRunner - .withPropertyValues("spring.mustache.prefix=classpath:/unknown/") + .withPropertyValues("spring.mustache.prefix=classpath:/unknown/", + "server.error.include-stacktrace=always") .run((context) -> { WebTestClient client = WebTestClient.bindToApplicationContext(context) .build(); @@ -194,7 +195,8 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests { .contentType(MediaType.TEXT_HTML).expectBody(String.class) .returnResult().getResponseBody(); assertThat(body).contains("Whitelabel Error Page") - .contains("
Expected!
"); + .contains("
Expected!
") + .contains("
java.lang.IllegalStateException"); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfigurationTests.java index a266abd972..19c6fd7dc9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfigurationTests.java @@ -56,8 +56,10 @@ public class ErrorMvcAutoConfigurationTests { new IllegalStateException("Exception message"), false); errorView.render(errorAttributes.getErrorAttributes(webRequest, true), webRequest.getRequest(), webRequest.getResponse()); - assertThat(((MockHttpServletResponse) webRequest.getResponse()) - .getContentAsString()).contains("
Exception message
"); + String responseString = ((MockHttpServletResponse) webRequest.getResponse()) + .getContentAsString(); + assertThat(responseString).contains("
Exception message
") + .contains("
java.lang.IllegalStateException"); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/NonRecursivePropertyPlaceholderHelperTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/NonRecursivePropertyPlaceholderHelperTests.java deleted file mode 100644 index c6ed3d129c..0000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/NonRecursivePropertyPlaceholderHelperTests.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2012-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.web.servlet.error; - -import java.util.Properties; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link NonRecursivePropertyPlaceholderHelper}. - * - * @author Phillip Webb - */ -public class NonRecursivePropertyPlaceholderHelperTests { - - private final NonRecursivePropertyPlaceholderHelper helper = new NonRecursivePropertyPlaceholderHelper( - "${", "}"); - - @Test - public void canResolve() { - Properties properties = new Properties(); - properties.put("a", "b"); - String result = this.helper.replacePlaceholders("${a}", properties); - assertThat(result).isEqualTo("b"); - } - - @Test - public void cannotResolveRecursive() { - Properties properties = new Properties(); - properties.put("a", "${b}"); - properties.put("b", "c"); - String result = this.helper.replacePlaceholders("${a}", properties); - assertThat(result).isEqualTo("${b}"); - } - -}