diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/DecoratedAssertionError.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/DecoratedAssertionError.java
new file mode 100644
index 0000000000000..85848a9949aba
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/DecoratedAssertionError.java
@@ -0,0 +1,61 @@
+package io.quarkus.vertx.http.runtime;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+
+/**
+ * Wraps an exception and prints stack trace with code snippets.
+ *
+ *
+ * To obtain the original exception, use {@link #getOriginal()}.
+ *
+ *
+ * NOTE: Copied from laech/java-stacksrc
+ */
+final class DecoratedAssertionError extends AssertionError {
+
+ private final Throwable original;
+ private final String decoratedStackTrace;
+
+ DecoratedAssertionError(Throwable original) {
+ this(original, null);
+ }
+
+ /**
+ * @param pruneStackTraceKeepFromClass if not null, will prune the stack traces, keeping only
+ * elements that are called directly or indirectly by this class
+ */
+ DecoratedAssertionError(
+ Throwable original, Class> pruneStackTraceKeepFromClass) {
+ this.original = original;
+ this.decoratedStackTrace = StackTraceDecorator.get().decorate(original, pruneStackTraceKeepFromClass);
+ setStackTrace(new StackTraceElement[0]);
+ }
+
+ @Override
+ public String getMessage() {
+ // Override this instead of calling the super(message) constructor, as super(null) will create
+ // the "null" string instead of actually being null
+ return getOriginal().getMessage();
+ }
+
+ /** Gets the original throwable being wrapped. */
+ public Throwable getOriginal() {
+ return original;
+ }
+
+ @Override
+ public void printStackTrace(PrintWriter out) {
+ out.println(this);
+ }
+
+ @Override
+ public void printStackTrace(PrintStream out) {
+ out.println(this);
+ }
+
+ @Override
+ public String toString() {
+ return decoratedStackTrace;
+ }
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FileCollector.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FileCollector.java
new file mode 100644
index 0000000000000..8fb5e5bd6dbce
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FileCollector.java
@@ -0,0 +1,49 @@
+package io.quarkus.vertx.http.runtime;
+
+import java.io.IOException;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * NOTE: Copied from laech/java-stacksrc
+ */
+final class FileCollector implements FileVisitor {
+
+ private final Map> result = new HashMap<>();
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
+ result.computeIfAbsent(file.getFileName().toString(), __ -> new ArrayList<>()).add(file);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFileFailed(Path file, IOException exc) {
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
+ return FileVisitResult.CONTINUE;
+ }
+
+ static Map> collect(Path dir) throws IOException {
+ var collector = new FileCollector();
+ Files.walkFileTree(dir, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, collector);
+ return collector.result;
+ }
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java
index a08fa7c6fd26b..e61b3ce61bccf 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java
@@ -128,6 +128,7 @@ public void accept(Throwable throwable) {
exception.addSuppressed(e);
}
if (showStack && exception != null) {
+ exception = new DecoratedAssertionError(exception);
details = generateHeaderMessage(exception, uuid);
stack = generateStackTrace(exception);
} else {
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StackTraceDecorator.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StackTraceDecorator.java
new file mode 100644
index 0000000000000..2f0a1a4b32c37
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StackTraceDecorator.java
@@ -0,0 +1,235 @@
+package io.quarkus.vertx.http.runtime;
+
+import static java.lang.String.format;
+import static java.lang.System.lineSeparator;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.IntStream;
+
+/**
+ *
+ * NOTE: Copied from laech/java-stacksrc
+ */
+final class StackTraceDecorator {
+ private StackTraceDecorator() {
+ }
+
+ private static final StackTraceDecorator instance = new StackTraceDecorator();
+
+ public static StackTraceDecorator get() {
+ return instance;
+ }
+
+ private static final int CONTEXT_LINE_COUNT = 2;
+
+ private volatile Map> cachedFiles;
+
+ private Map> cachedFiles() throws IOException {
+ if (cachedFiles == null) {
+ cachedFiles = FileCollector.collect(Paths.get(""));
+ }
+ return cachedFiles;
+ }
+
+ public String decorate(Throwable e) {
+ return decorate(e, null);
+ }
+
+ public String decorate(Throwable e, Class> keepFromClass) {
+ if (keepFromClass != null) {
+ pruneStackTrace(e, keepFromClass, new HashSet<>());
+ }
+
+ var stackTrace = getStackTraceAsString(e);
+ try {
+
+ var alreadySeenElements = new HashSet();
+ var alreadySeenSnippets = new HashSet>();
+ stackTrace = decorate(e, stackTrace, 1, alreadySeenElements, alreadySeenSnippets);
+
+ var cause = e.getCause();
+ if (cause != null) {
+ stackTrace = decorate(cause, stackTrace, 1, alreadySeenElements, alreadySeenSnippets);
+ }
+
+ for (var suppressed : e.getSuppressed()) {
+ stackTrace = decorate(suppressed, stackTrace, 2, alreadySeenElements, alreadySeenSnippets);
+ }
+
+ } catch (Exception sup) {
+ e.addSuppressed(sup);
+ }
+ return stackTrace;
+ }
+
+ private String decorate(
+ Throwable e,
+ String stackTrace,
+ int indentLevel,
+ Set alreadySeenElements,
+ Set> alreadySeenSnippets)
+ throws IOException {
+
+ for (var element : e.getStackTrace()) {
+ if (!alreadySeenElements.add(element)) {
+ continue;
+ }
+
+ var snippet = decorate(element);
+ if (snippet.isEmpty() || !alreadySeenSnippets.add(snippet.get())) {
+ // Don't print the same snippet multiple times,
+ // multiple lambda on one line can create this situation
+ continue;
+ }
+
+ var line = element.toString();
+ var indent = "\t".repeat(indentLevel);
+ var replacement = String.format(
+ "%s%n%n%s%n%n",
+ line, snippet.get().stream().collect(joining(lineSeparator() + indent, indent, "")));
+ stackTrace = stackTrace.replace(line, replacement);
+ }
+ return stackTrace;
+ }
+
+ private Optional> decorate(StackTraceElement element) throws IOException {
+ var file = findFile(element);
+ if (file.isEmpty()) {
+ return Optional.empty();
+ }
+
+ var lines = readContextLines(element, file.get());
+ if (lines.isEmpty()) {
+ return Optional.empty();
+ }
+
+ removeBlankLinesFromStart(lines);
+ removeBlankLinesFromEnd(lines);
+ return Optional.of(buildSnippet(lines, element));
+ }
+
+ private Optional findFile(StackTraceElement element) throws IOException {
+ if (element.getLineNumber() < 1
+ || element.getFileName() == null
+ || element.getMethodName().startsWith("access$")) { // Ignore class entry lines
+ return Optional.empty();
+ }
+
+ var tail = withPackagePath(element);
+ var paths = cachedFiles().getOrDefault(element.getFileName(), List.of());
+ var exact = paths.stream().filter(it -> it.endsWith(tail)).findAny();
+ if (exact.isPresent() || element.getFileName().endsWith(".java")) {
+ return exact;
+ }
+ return Optional.ofNullable(paths.size() == 1 ? paths.get(0) : null);
+ }
+
+ private Path withPackagePath(StackTraceElement element) {
+ var fileName = requireNonNull(element.getFileName());
+ var className = element.getClassName();
+ var i = className.lastIndexOf(".");
+ var parent = i < 0 ? "" : className.substring(0, i).replace('.', '/');
+ return Paths.get(parent).resolve(fileName);
+ }
+
+ private NavigableMap readContextLines(StackTraceElement elem, Path path)
+ throws IOException {
+
+ var startLineNum = Math.max(1, elem.getLineNumber() - CONTEXT_LINE_COUNT);
+ try (var stream = Files.lines(path)) {
+
+ var lines = stream
+ .limit(elem.getLineNumber() + CONTEXT_LINE_COUNT)
+ .skip(startLineNum - 1)
+ .collect(toList());
+
+ return IntStream.range(0, lines.size())
+ .boxed()
+ .reduce(
+ new TreeMap<>(),
+ (acc, i) -> {
+ acc.put(i + startLineNum, lines.get(i));
+ return acc;
+ },
+ (a, b) -> b);
+ }
+ }
+
+ @SuppressWarnings("NullAway")
+ private void removeBlankLinesFromStart(NavigableMap lines) {
+ IntStream.rangeClosed(lines.firstKey(), lines.lastKey())
+ .takeWhile(i -> lines.get(i).isBlank())
+ .forEach(lines::remove);
+ }
+
+ @SuppressWarnings("NullAway")
+ private void removeBlankLinesFromEnd(NavigableMap lines) {
+ IntStream.iterate(lines.lastKey(), i -> i >= lines.firstKey(), i -> i - 1)
+ .takeWhile(i -> lines.get(i).isBlank())
+ .forEach(lines::remove);
+ }
+
+ private static List buildSnippet(
+ NavigableMap lines, StackTraceElement elem) {
+ var maxLineNumWidth = String.valueOf(lines.lastKey()).length();
+ return lines.entrySet().stream()
+ .map(
+ entry -> {
+ var lineNum = entry.getKey();
+ var isTarget = lineNum == elem.getLineNumber();
+ var line = entry.getValue();
+ var lineNumStr = format("%" + maxLineNumWidth + "d", lineNum);
+ return format(
+ "%s %s%s", isTarget ? "->" : " ", lineNumStr, line.isEmpty() ? "" : " " + line);
+ })
+ .collect(toList());
+ }
+
+ private static String getStackTraceAsString(Throwable e) {
+ var stringWriter = new StringWriter();
+ var printWriter = new PrintWriter(stringWriter);
+ e.printStackTrace(printWriter);
+ printWriter.flush();
+ return stringWriter.toString();
+ }
+
+ private static void pruneStackTrace(
+ Throwable throwable, Class> keepFromClass, Set alreadySeen) {
+ if (!alreadySeen.add(throwable)) {
+ return;
+ }
+
+ var stackTrace = throwable.getStackTrace();
+ for (var i = stackTrace.length - 1; i >= 0; i--) {
+ if (stackTrace[i].getClassName().equals(keepFromClass.getName())) {
+ throwable.setStackTrace(Arrays.copyOfRange(stackTrace, 0, i + 1));
+ break;
+ }
+ }
+
+ var cause = throwable.getCause();
+ if (cause != null) {
+ pruneStackTrace(cause, keepFromClass, alreadySeen);
+ }
+
+ for (var suppressed : throwable.getSuppressed()) {
+ pruneStackTrace(suppressed, keepFromClass, alreadySeen);
+ }
+ }
+}