Skip to content

Commit 2c3e8de

Browse files
committed
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
1 parent 30cd86b commit 2c3e8de

File tree

6 files changed

+43
-217
lines changed

6 files changed

+43
-217
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,9 @@ private Resource resolveResource(String viewName) {
189189
protected Mono<ServerResponse> renderDefaultErrorView(
190190
ServerResponse.BodyBuilder responseBody, Map<String, Object> error) {
191191
StringBuilder builder = new StringBuilder();
192-
Object message = error.get("message");
193192
Date timestamp = (Date) error.get("timestamp");
193+
Object message = error.get("message");
194+
Object trace = error.get("trace");
194195
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
195196
"<p>This application has no configured error view, so you are seeing this as a fallback.</p>")
196197
.append("<div id='created'>").append(timestamp).append("</div>")
@@ -200,6 +201,9 @@ protected Mono<ServerResponse> renderDefaultErrorView(
200201
if (message != null) {
201202
builder.append("<div>").append(htmlEscape(message)).append("</div>");
202203
}
204+
if (trace != null) {
205+
builder.append("<div>").append(htmlEscape(trace)).append("</div>");
206+
}
203207
builder.append("</body></html>");
204208
return responseBody.syncBody(builder.toString());
205209
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.java

Lines changed: 30 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616

1717
package org.springframework.boot.autoconfigure.web.servlet.error;
1818

19-
import java.util.Collections;
20-
import java.util.HashMap;
19+
import java.util.Date;
2120
import java.util.List;
2221
import java.util.Map;
2322
import java.util.stream.Collectors;
@@ -66,14 +65,8 @@
6665
import org.springframework.context.annotation.ConditionContext;
6766
import org.springframework.context.annotation.Conditional;
6867
import org.springframework.context.annotation.Configuration;
69-
import org.springframework.context.expression.MapAccessor;
7068
import org.springframework.core.Ordered;
7169
import org.springframework.core.type.AnnotatedTypeMetadata;
72-
import org.springframework.expression.EvaluationContext;
73-
import org.springframework.expression.Expression;
74-
import org.springframework.expression.spel.standard.SpelExpressionParser;
75-
import org.springframework.expression.spel.support.SimpleEvaluationContext;
76-
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
7770
import org.springframework.web.servlet.DispatcherServlet;
7871
import org.springframework.web.servlet.View;
7972
import org.springframework.web.servlet.view.BeanNameViewResolver;
@@ -86,6 +79,7 @@
8679
* @author Dave Syer
8780
* @author Andy Wilkinson
8881
* @author Stephane Nicoll
82+
* @author Brian Clozel
8983
*/
9084
@Configuration
9185
@ConditionalOnWebApplication(type = Type.SERVLET)
@@ -163,12 +157,7 @@ public DefaultErrorViewResolver conventionErrorViewResolver() {
163157
@Conditional(ErrorTemplateMissingCondition.class)
164158
protected static class WhitelabelErrorViewConfiguration {
165159

166-
private final SpelView defaultErrorView = new SpelView(
167-
"<html><body><h1>Whitelabel Error Page</h1>"
168-
+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
169-
+ "<div id='created'>${timestamp}</div>"
170-
+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
171-
+ "<div>${message}</div></body></html>");
160+
private final StaticView defaultErrorView = new StaticView();
172161

173162
@Bean(name = "error")
174163
@ConditionalOnMissingBean(name = "error")
@@ -214,27 +203,11 @@ public ConditionOutcome getMatchOutcome(ConditionContext context,
214203
}
215204

216205
/**
217-
* Simple {@link View} implementation that resolves variables as SpEL expressions.
206+
* Simple {@link View} implementation that writes a default HTML error page.
218207
*/
219-
private static class SpelView implements View {
208+
private static class StaticView implements View {
220209

221-
private static final Log logger = LogFactory.getLog(SpelView.class);
222-
223-
private final NonRecursivePropertyPlaceholderHelper helper;
224-
225-
private final String template;
226-
227-
private volatile Map<String, Expression> expressions;
228-
229-
SpelView(String template) {
230-
this.helper = new NonRecursivePropertyPlaceholderHelper("${", "}");
231-
this.template = template;
232-
}
233-
234-
@Override
235-
public String getContentType() {
236-
return "text/html";
237-
}
210+
private static final Log logger = LogFactory.getLog(StaticView.class);
238211

239212
@Override
240213
public void render(Map<String, ?> model, HttpServletRequest request,
@@ -244,13 +217,31 @@ public void render(Map<String, ?> model, HttpServletRequest request,
244217
logger.error(message);
245218
return;
246219
}
220+
StringBuilder builder = new StringBuilder();
221+
Date timestamp = (Date) model.get("timestamp");
222+
Object message = model.get("message");
223+
Object trace = model.get("trace");
247224
if (response.getContentType() == null) {
248225
response.setContentType(getContentType());
249226
}
250-
PlaceholderResolver resolver = new ExpressionResolver(getExpressions(),
251-
model);
252-
String result = this.helper.replacePlaceholders(this.template, resolver);
253-
response.getWriter().append(result);
227+
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
228+
"<p>This application has no configured error view, so you are seeing this as a fallback.</p>")
229+
.append("<div id='created'>").append(timestamp).append("</div>")
230+
.append("<div>There was an unexpected error (type=")
231+
.append(htmlEscape(model.get("error"))).append(", status=")
232+
.append(htmlEscape(model.get("status"))).append(").</div>");
233+
if (message != null) {
234+
builder.append("<div>").append(htmlEscape(message)).append("</div>");
235+
}
236+
if (trace != null) {
237+
builder.append("<div>").append(htmlEscape(trace)).append("</div>");
238+
}
239+
builder.append("</body></html>");
240+
response.getWriter().append(builder.toString());
241+
}
242+
243+
private String htmlEscape(Object input) {
244+
return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
254245
}
255246

256247
private String getMessage(Map<String, ?> model) {
@@ -264,69 +255,9 @@ private String getMessage(Map<String, ?> model) {
264255
return message;
265256
}
266257

267-
private Map<String, Expression> getExpressions() {
268-
if (this.expressions == null) {
269-
synchronized (this) {
270-
ExpressionCollector expressionCollector = new ExpressionCollector();
271-
this.helper.replacePlaceholders(this.template, expressionCollector);
272-
this.expressions = expressionCollector.getExpressions();
273-
}
274-
}
275-
return this.expressions;
276-
}
277-
278-
}
279-
280-
/**
281-
* {@link PlaceholderResolver} to collect placeholder expressions.
282-
*/
283-
private static class ExpressionCollector implements PlaceholderResolver {
284-
285-
private final SpelExpressionParser parser = new SpelExpressionParser();
286-
287-
private final Map<String, Expression> expressions = new HashMap<>();
288-
289-
@Override
290-
public String resolvePlaceholder(String name) {
291-
this.expressions.put(name, this.parser.parseExpression(name));
292-
return null;
293-
}
294-
295-
public Map<String, Expression> getExpressions() {
296-
return Collections.unmodifiableMap(this.expressions);
297-
}
298-
299-
}
300-
301-
/**
302-
* SpEL based {@link PlaceholderResolver}.
303-
*/
304-
private static class ExpressionResolver implements PlaceholderResolver {
305-
306-
private final Map<String, Expression> expressions;
307-
308-
private final EvaluationContext context;
309-
310-
ExpressionResolver(Map<String, Expression> expressions, Map<String, ?> map) {
311-
this.expressions = expressions;
312-
this.context = getContext(map);
313-
}
314-
315-
private EvaluationContext getContext(Map<String, ?> map) {
316-
return SimpleEvaluationContext.forPropertyAccessors(new MapAccessor())
317-
.withRootObject(map).build();
318-
}
319-
320258
@Override
321-
public String resolvePlaceholder(String placeholderName) {
322-
Expression expression = this.expressions.get(placeholderName);
323-
Object expressionValue = (expression != null)
324-
? expression.getValue(this.context) : null;
325-
return escape(expressionValue);
326-
}
327-
328-
private String escape(Object value) {
329-
return HtmlUtils.htmlEscape((value != null) ? value.toString() : null);
259+
public String getContentType() {
260+
return "text/html";
330261
}
331262

332263
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/NonRecursivePropertyPlaceholderHelper.java

Lines changed: 0 additions & 61 deletions
This file was deleted.

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerIntegrationTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,8 @@ public void statusException() {
184184
@Test
185185
public void defaultErrorView() {
186186
this.contextRunner
187-
.withPropertyValues("spring.mustache.prefix=classpath:/unknown/")
187+
.withPropertyValues("spring.mustache.prefix=classpath:/unknown/",
188+
"server.error.include-stacktrace=always")
188189
.run((context) -> {
189190
WebTestClient client = WebTestClient.bindToApplicationContext(context)
190191
.build();
@@ -194,7 +195,8 @@ public void defaultErrorView() {
194195
.contentType(MediaType.TEXT_HTML).expectBody(String.class)
195196
.returnResult().getResponseBody();
196197
assertThat(body).contains("Whitelabel Error Page")
197-
.contains("<div>Expected!</div>");
198+
.contains("<div>Expected!</div>")
199+
.contains("<div>java.lang.IllegalStateException");
198200
});
199201
}
200202

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfigurationTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ public void renderContainsViewWithExceptionDetails() throws Exception {
5656
new IllegalStateException("Exception message"), false);
5757
errorView.render(errorAttributes.getErrorAttributes(webRequest, true),
5858
webRequest.getRequest(), webRequest.getResponse());
59-
assertThat(((MockHttpServletResponse) webRequest.getResponse())
60-
.getContentAsString()).contains("<div>Exception message</div>");
59+
String responseString = ((MockHttpServletResponse) webRequest.getResponse())
60+
.getContentAsString();
61+
assertThat(responseString).contains("<div>Exception message</div>")
62+
.contains("<div>java.lang.IllegalStateException");
6163
});
6264
}
6365

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/NonRecursivePropertyPlaceholderHelperTests.java

Lines changed: 0 additions & 52 deletions
This file was deleted.

0 commit comments

Comments
 (0)