Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package io.quarkus.deployment.devmode;

import java.io.PrintWriter;
import java.io.StringWriter;

import io.quarkus.runtime.TemplateHtmlBuilder;

/**
Expand All @@ -14,18 +11,11 @@ public static String generateHtml(final Throwable exception) {
TemplateHtmlBuilder builder = new TemplateHtmlBuilder("Error restarting Quarkus", exception.getClass().getName(),
generateHeaderMessage(exception));

builder.stack(generateStackTrace(exception));
builder.stack(exception);

return builder.toString();
}

private static String generateStackTrace(final Throwable exception) {
StringWriter stringWriter = new StringWriter();
exception.printStackTrace(new PrintWriter(stringWriter));

return stringWriter.toString().trim();
}

private static String generateHeaderMessage(final Throwable exception) {
return String.format("%s: %s", exception.getClass().getName(), extractFirstLine(exception.getMessage()));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
package io.quarkus.runtime;

import io.quarkus.runtime.util.ExceptionUtil;

public class TemplateHtmlBuilder {

private static final String SCRIPT_STACKTRACE_MANIPULATION = "<script>\n" +
" function toggleStackTraceOrder() {\n" +
" var stElement = document.getElementById('stacktrace');\n" +
" var current = stElement.getAttribute('data-current-setting');\n" +
" if (current == 'original-stacktrace') {\n" +
" var reverseOrder = document.getElementById('reversed-stacktrace');\n" +
" stElement.innerHTML = reverseOrder.innerHTML;\n" +
" stElement.setAttribute('data-current-setting', 'reversed-stacktrace');\n" +
" } else {\n" +
" var originalOrder = document.getElementById('original-stacktrace');\n" +
" stElement.innerHTML = originalOrder.innerHTML;\n" +
" stElement.setAttribute('data-current-setting', 'original-stacktrace');\n" +
" }\n" +
" return;\n" +
" }\n" +
" function showDefaultStackTraceOrder() {\n" +
" var reverseOrder = document.getElementById('reversed-stacktrace');\n" +
" var stElement = document.getElementById('stacktrace');\n" +
" if (reverseOrder == null || stElement == null) {\n" +
" return;\n" +
" }\n" +
" // default to reverse ordered stacktrace\n" +
" stElement.innerHTML = reverseOrder.innerHTML;\n" +
" stElement.setAttribute('data-current-setting', 'reversed-stacktrace');\n" +
" return;\n" +
" }\n" +
"</script>\n";

private static final String HTML_TEMPLATE_START = "" +
"<!doctype html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <title>%1$s%2$s</title>\n" +
" <meta charset=\"utf-8\">\n" +
" <style>%3$s</style>\n" +
SCRIPT_STACKTRACE_MANIPULATION +
"</head>\n" +
"<body>\n";
"<body onload=\"showDefaultStackTraceOrder()\">\n";

private static final String HTML_TEMPLATE_END = "</div></body>\n" +
"</html>\n";
Expand Down Expand Up @@ -43,7 +74,18 @@ public class TemplateHtmlBuilder {

private static final String RESOURCES_END = "</div>";

private static final String ERROR_STACK = " <div class=\"trace\">\n" +
private static final String STACKTRACE_DISPLAY_DIV = "<div id=\"stacktrace\"></div>";

private static final String ERROR_STACK = " <div id=\"original-stacktrace\" class=\"trace hidden\">\n" +
"<p><em><a href=\"\" onClick=\"toggleStackTraceOrder(); return false;\">Click Here</a> " +
"to see the stacktrace in reversed order (root-cause first)</em></p>" +
" <pre>%1$s</pre>\n" +
" </div>\n";

private static final String ERROR_STACK_REVERSED = " <div id=\"reversed-stacktrace\" class=\"trace hidden\">\n" +
"<p><em>The stacktrace below has been reversed to show the root cause first. " +
"<a href=\"\" onClick=\"toggleStackTraceOrder(); return false;\">Click Here</a> " +
"to see the original stacktrace</em></p>" +
" <pre>%1$s</pre>\n" +
" </div>\n";

Expand Down Expand Up @@ -128,6 +170,9 @@ public class TemplateHtmlBuilder {
".trace {\n" +
" overflow-y: scroll;\n" +
"}\n" +
".hidden {\n" +
" display: none;\n" +
"}\n" +
"\n" +
"pre {\n" +
" white-space: pre;\n" +
Expand All @@ -145,8 +190,10 @@ public TemplateHtmlBuilder(String title, String subTitle, String details) {
result.append(String.format(HEADER_TEMPLATE, escapeHtml(title), escapeHtml(details)));
}

public TemplateHtmlBuilder stack(String stack) {
result.append(String.format(ERROR_STACK, escapeHtml(stack)));
public TemplateHtmlBuilder stack(final Throwable throwable) {
result.append(String.format(ERROR_STACK, escapeHtml(ExceptionUtil.generateStackTrace(throwable))));
result.append(String.format(ERROR_STACK_REVERSED, escapeHtml(ExceptionUtil.rootCauseFirstStackTrace(throwable))));
result.append(STACKTRACE_DISPLAY_DIV);
return this;
}

Expand Down Expand Up @@ -236,4 +283,13 @@ private static String escapeHtml(final String bodyText) {
.replace("<", "&lt;")
.replace(">", "&gt;");
}

private static String extractFirstLine(final String message) {
if (null == message) {
return "";
}

String[] lines = message.split("\\r?\\n");
return lines[0].trim();
}
}
116 changes: 116 additions & 0 deletions core/runtime/src/main/java/io/quarkus/runtime/util/ExceptionUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package io.quarkus.runtime.util;

import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;

/**
*
*/
public class ExceptionUtil {

/**
* Returns the string representation of the stacktrace of the passed {@code exception}
*
* @param exception
* @return
*/
public static String generateStackTrace(final Throwable exception) {
if (exception == null) {
return null;
}
final StringWriter stringWriter = new StringWriter();
exception.printStackTrace(new PrintWriter(stringWriter));

return stringWriter.toString().trim();
}

/**
* Returns a "upside down" stacktrace of the {@code exception} with the root
* cause showing up first in the stacktrace.
* <em>Note:</em> This is a relatively expensive method because it creates additional
* exceptions and manipulates their stacktraces. Care should be taken to determine whether
* usage of this method is necessary.
*
* @param exception The exception
* @return
*/
public static String rootCauseFirstStackTrace(final Throwable exception) {
if (exception == null) {
return null;
}
// create an exception chain with the root cause being at element 0
final List<Throwable> exceptionChain = new ArrayList<>();
Throwable curr = exception;
while (curr != null) {
exceptionChain.add(0, curr);
curr = curr.getCause();
}
Throwable prevStrippedCause = null;
Throwable modifiedRoot = null;
// We reverse the stacktrace as follows:
// - Iterate the exception chain that we created, which has the root cause at element 0
// - for each exception in this chain
// - create a new "copy" C1 of that exception
// - create a new copy C2 of the "next" exception in the chain with its cause stripped off
// - C1.initCause(C2)
// - keep track of the copy C1 of the first element in the exception chain. That C1, lets call
// it RC1, will be the modified representation of the root cause on which if printStackTrace()
// is called, then it will end up printing stacktrace in reverse order (because of the way we
// fiddled around with its causes and other details)
// - Finally, replace the occurrences of "Caused by:" string the in the stacktrace to "Resulted in:"
// to better phrase the reverse stacktrace representation.
for (int i = 0; i < exceptionChain.size(); i++) {
final Throwable x = prevStrippedCause == null ? stripCause(exceptionChain.get(0)) : prevStrippedCause;
if (i != exceptionChain.size() - 1) {
final Throwable strippedCause = stripCause(exceptionChain.get(i + 1));
x.initCause(strippedCause);
prevStrippedCause = strippedCause;
}
if (i == 0) {
modifiedRoot = x;
}
}
return generateStackTrace(modifiedRoot).replace("Caused by:", "Resulted in:");
}

/**
* Creates and returns a new {@link Throwable} which has the following characteristics:
* <ul>
* <li>The {@code cause} of the Throwable hasn't yet been {@link Throwable#initCause(Throwable) inited}
* and thus can be "inited" later on if needed
* </li>
* <li>
* The stacktrace elements of the Throwable have been set to the stacktrace elements of the passed
* {@code t}. That way, any call to {@link Throwable#printStackTrace(PrintStream)} for example
* will print the stacktrace of the passed {@code t}
* </li>
* </ul>
*
* @param t The exception
* @return
*/
private static Throwable stripCause(final Throwable t) {
final Throwable stripped = delegatingToStringThrowable(t);
stripped.setStackTrace(t.getStackTrace());
return stripped;
}

/**
* Creates and returns a new {@link Throwable} whose {@link Throwable#toString()} has been
* overridden to call the {@code toString()} method of the passed {@code t}.
*
* @param t The exception
* @return
*/
private static Throwable delegatingToStringThrowable(final Throwable t) {
return new Throwable() {
@Override
public String toString() {
return t.toString();
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.quarkus.runtime.util;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOError;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Test;

/**
*
*/
public class ExceptionUtilTest {

/**
* Tests the {@link ExceptionUtil#rootCauseFirstStackTrace(Throwable)} method
*
* @throws Exception
*/
@Test
public void testReversed() throws Exception {
final Throwable ex = generateException();
final String rootCauseFirst = ExceptionUtil.rootCauseFirstStackTrace(ex);
assertNotNull(rootCauseFirst, "Stacktrace was null");
assertTrue(rootCauseFirst.contains("Resulted in:"),
"Stacktrace doesn't contain the \"Resulted in:\" string");
assertFalse(rootCauseFirst.contains("Caused by:"), "Stacktrace contains the \"Caused by:\" string");
final String[] lines = rootCauseFirst.split("\n");
final String firstLine = lines[0];
assertTrue(firstLine.startsWith(NumberFormatException.class.getName() + ": For input string: \"23.23232\""),
"Unexpected root cause");
final List<String> expectedResultedIns = new ArrayList<>();
expectedResultedIns.add(IllegalArgumentException.class.getName() + ": Incorrect param");
expectedResultedIns.add(IOException.class.getName() + ": Request processing failed");
expectedResultedIns.add(IOError.class.getName());
expectedResultedIns.add(RuntimeException.class.getName() + ": Unexpected exception");
for (final String line : lines) {
if (!line.startsWith("Resulted in:")) {
continue;
}
final String expected = expectedResultedIns.remove(0);
assertTrue(line.startsWith("Resulted in: " + expected), "Unexpected stacktrace element '" + line + "'");
}
assertTrue(expectedResultedIns.isEmpty(), "Reversed stacktrace is missing certain elements");
}

private Throwable generateException() {
try {
try {
Integer.parseInt("23.23232");
} catch (NumberFormatException nfe) {
throw new IllegalArgumentException("Incorrect param", nfe);
}
} catch (IllegalArgumentException iae) {
try {
throw new IOException("Request processing failed", iae);
} catch (IOException e) {
try {
throw new IOError(e);
} catch (IOError ie) {
return new RuntimeException("Unexpected exception", ie);
}
}
}
throw new RuntimeException("Should not reach here");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public void testHtmlError() {
RestAssured.when().get("/error").then()
.statusCode(500)
.body(containsString("<h1 class=\"container\">Internal Server Error</h1>"))
.body(containsString("<div class=\"trace\">"));
.body(containsString("<div id=\"stacktrace\">"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws
if (errorMessage != null) {
details = errorMessage;
}
if (Boolean.parseBoolean(getInitParameter(SHOW_STACK)) && exception != null) {
final boolean showStack = Boolean.parseBoolean(getInitParameter(SHOW_STACK));
if (showStack && exception != null) {
details = generateHeaderMessage(exception, uuid == null ? null : uuid.toString());
stack = generateStackTrace(exception);

Expand All @@ -47,7 +48,11 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws
//We default to HTML representation
resp.setContentType("text/html");
resp.setCharacterEncoding(StandardCharsets.UTF_8.name());
resp.getWriter().write(new TemplateHtmlBuilder("Internal Server Error", details, details).stack(stack).toString());
final TemplateHtmlBuilder htmlBuilder = new TemplateHtmlBuilder("Internal Server Error", details, details);
if (showStack && exception != null) {
htmlBuilder.stack(exception);
}
resp.getWriter().write(htmlBuilder.toString());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ public void handle(RoutingContext event) {
} else {
//We default to HTML representation
event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8");
event.response().end(new TemplateHtmlBuilder("Internal Server Error", details, details).stack(stack).toString());
final TemplateHtmlBuilder htmlBuilder = new TemplateHtmlBuilder("Internal Server Error", details, details);
if (showStack && exception != null) {
htmlBuilder.stack(exception);
}
event.response().end(htmlBuilder.toString());
}
}

Expand Down