Include "trace" info in whitelabel error views

This commit shows the stacktrace information in default WhiteLabel error
views for Spring MVC and Spring WebFlux.

This information is only shown if it is present in the model map, which
depends on the `server.error.include-stacktrace` configuration property.

Closes gh-12838
pull/14836/head
Brian Clozel 6 years ago
parent 30cd86bab5
commit 2c3e8de959

@ -189,8 +189,9 @@ public abstract class AbstractErrorWebExceptionHandler
protected Mono<ServerResponse> renderDefaultErrorView( protected Mono<ServerResponse> renderDefaultErrorView(
ServerResponse.BodyBuilder responseBody, Map<String, Object> error) { ServerResponse.BodyBuilder responseBody, Map<String, Object> error) {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
Object message = error.get("message");
Date timestamp = (Date) error.get("timestamp"); Date timestamp = (Date) error.get("timestamp");
Object message = error.get("message");
Object trace = error.get("trace");
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append( builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
"<p>This application has no configured error view, so you are seeing this as a fallback.</p>") "<p>This application has no configured error view, so you are seeing this as a fallback.</p>")
.append("<div id='created'>").append(timestamp).append("</div>") .append("<div id='created'>").append(timestamp).append("</div>")
@ -200,6 +201,9 @@ public abstract class AbstractErrorWebExceptionHandler
if (message != null) { if (message != null) {
builder.append("<div>").append(htmlEscape(message)).append("</div>"); builder.append("<div>").append(htmlEscape(message)).append("</div>");
} }
if (trace != null) {
builder.append("<div>").append(htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>"); builder.append("</body></html>");
return responseBody.syncBody(builder.toString()); return responseBody.syncBody(builder.toString());
} }

@ -16,8 +16,7 @@
package org.springframework.boot.autoconfigure.web.servlet.error; package org.springframework.boot.autoconfigure.web.servlet.error;
import java.util.Collections; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; 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.ConditionContext;
import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.expression.MapAccessor;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.type.AnnotatedTypeMetadata; 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.DispatcherServlet;
import org.springframework.web.servlet.View; import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.BeanNameViewResolver; import org.springframework.web.servlet.view.BeanNameViewResolver;
@ -86,6 +79,7 @@ import org.springframework.web.util.HtmlUtils;
* @author Dave Syer * @author Dave Syer
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Brian Clozel
*/ */
@Configuration @Configuration
@ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnWebApplication(type = Type.SERVLET)
@ -163,12 +157,7 @@ public class ErrorMvcAutoConfiguration {
@Conditional(ErrorTemplateMissingCondition.class) @Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration { protected static class WhitelabelErrorViewConfiguration {
private final SpelView defaultErrorView = new SpelView( private final StaticView defaultErrorView = new StaticView();
"<html><body><h1>Whitelabel Error Page</h1>"
+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
+ "<div id='created'>${timestamp}</div>"
+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
+ "<div>${message}</div></body></html>");
@Bean(name = "error") @Bean(name = "error")
@ConditionalOnMissingBean(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 static final Log logger = LogFactory.getLog(StaticView.class);
private final NonRecursivePropertyPlaceholderHelper helper;
private final String template;
private volatile Map<String, Expression> expressions;
SpelView(String template) {
this.helper = new NonRecursivePropertyPlaceholderHelper("${", "}");
this.template = template;
}
@Override
public String getContentType() {
return "text/html";
}
@Override @Override
public void render(Map<String, ?> model, HttpServletRequest request, public void render(Map<String, ?> model, HttpServletRequest request,
@ -244,13 +217,31 @@ public class ErrorMvcAutoConfiguration {
logger.error(message); logger.error(message);
return; 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) { if (response.getContentType() == null) {
response.setContentType(getContentType()); response.setContentType(getContentType());
} }
PlaceholderResolver resolver = new ExpressionResolver(getExpressions(), builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
model); "<p>This application has no configured error view, so you are seeing this as a fallback.</p>")
String result = this.helper.replacePlaceholders(this.template, resolver); .append("<div id='created'>").append(timestamp).append("</div>")
response.getWriter().append(result); .append("<div>There was an unexpected error (type=")
.append(htmlEscape(model.get("error"))).append(", status=")
.append(htmlEscape(model.get("status"))).append(").</div>");
if (message != null) {
builder.append("<div>").append(htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div>").append(htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
}
private String htmlEscape(Object input) {
return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
} }
private String getMessage(Map<String, ?> model) { private String getMessage(Map<String, ?> model) {
@ -264,69 +255,9 @@ public class ErrorMvcAutoConfiguration {
return message; return message;
} }
private Map<String, Expression> 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<String, Expression> expressions = new HashMap<>();
@Override
public String resolvePlaceholder(String name) {
this.expressions.put(name, this.parser.parseExpression(name));
return null;
}
public Map<String, Expression> getExpressions() {
return Collections.unmodifiableMap(this.expressions);
}
}
/**
* SpEL based {@link PlaceholderResolver}.
*/
private static class ExpressionResolver implements PlaceholderResolver {
private final Map<String, Expression> expressions;
private final EvaluationContext context;
ExpressionResolver(Map<String, Expression> expressions, Map<String, ?> map) {
this.expressions = expressions;
this.context = getContext(map);
}
private EvaluationContext getContext(Map<String, ?> map) {
return SimpleEvaluationContext.forPropertyAccessors(new MapAccessor())
.withRootObject(map).build();
}
@Override @Override
public String resolvePlaceholder(String placeholderName) { public String getContentType() {
Expression expression = this.expressions.get(placeholderName); return "text/html";
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);
} }
} }

@ -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<String> 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);
}
}
}

@ -184,7 +184,8 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests {
@Test @Test
public void defaultErrorView() { public void defaultErrorView() {
this.contextRunner this.contextRunner
.withPropertyValues("spring.mustache.prefix=classpath:/unknown/") .withPropertyValues("spring.mustache.prefix=classpath:/unknown/",
"server.error.include-stacktrace=always")
.run((context) -> { .run((context) -> {
WebTestClient client = WebTestClient.bindToApplicationContext(context) WebTestClient client = WebTestClient.bindToApplicationContext(context)
.build(); .build();
@ -194,7 +195,8 @@ public class DefaultErrorWebExceptionHandlerIntegrationTests {
.contentType(MediaType.TEXT_HTML).expectBody(String.class) .contentType(MediaType.TEXT_HTML).expectBody(String.class)
.returnResult().getResponseBody(); .returnResult().getResponseBody();
assertThat(body).contains("Whitelabel Error Page") assertThat(body).contains("Whitelabel Error Page")
.contains("<div>Expected!</div>"); .contains("<div>Expected!</div>")
.contains("<div>java.lang.IllegalStateException");
}); });
} }

@ -56,8 +56,10 @@ public class ErrorMvcAutoConfigurationTests {
new IllegalStateException("Exception message"), false); new IllegalStateException("Exception message"), false);
errorView.render(errorAttributes.getErrorAttributes(webRequest, true), errorView.render(errorAttributes.getErrorAttributes(webRequest, true),
webRequest.getRequest(), webRequest.getResponse()); webRequest.getRequest(), webRequest.getResponse());
assertThat(((MockHttpServletResponse) webRequest.getResponse()) String responseString = ((MockHttpServletResponse) webRequest.getResponse())
.getContentAsString()).contains("<div>Exception message</div>"); .getContentAsString();
assertThat(responseString).contains("<div>Exception message</div>")
.contains("<div>java.lang.IllegalStateException");
}); });
} }

@ -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}");
}
}
Loading…
Cancel
Save