diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FailureBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FailureBuildItem.java new file mode 100644 index 00000000000000..6c4e9a01fad10d --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FailureBuildItem.java @@ -0,0 +1,27 @@ +package io.quarkus.vertx.http.deployment; + +import io.quarkus.builder.item.MultiBuildItem; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * A handler that is applied to every route + */ +public final class FailureBuildItem extends MultiBuildItem { + + private final Handler handler; + + /** + * Creates a new instance of {@link FailureBuildItem}. + * + * @param handler the handler, if {@code null} the filter won't be used. + */ + public FailureBuildItem(Handler handler) { + this.handler = handler; + } + + public Handler getHandler() { + return handler; + } + +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index 2ec89082b8ba18..640424b33aa8ae 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -34,6 +34,7 @@ import io.quarkus.vertx.http.runtime.RouterProducer; import io.quarkus.vertx.http.runtime.VertxHttpRecorder; import io.quarkus.vertx.http.runtime.cors.CORSRecorder; +import io.quarkus.vertx.http.runtime.error.ErrorRecorder; import io.quarkus.vertx.http.runtime.filters.Filter; import io.vertx.core.impl.VertxImpl; import io.vertx.ext.web.Router; @@ -50,6 +51,12 @@ HttpRootPathBuildItem httpRoot(HttpBuildTimeConfig httpBuildTimeConfig) { FilterBuildItem cors(CORSRecorder recorder, HttpConfiguration configuration) { return new FilterBuildItem(recorder.corsHandler(configuration), FilterBuildItem.CORS); } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + FailureBuildItem errors(ErrorRecorder recorder, HttpConfiguration configuration, LaunchModeBuildItem launchMode) { + return new FailureBuildItem(recorder.errorHandler(configuration, launchMode.getLaunchMode())); + } @BuildStep AdditionalBeanBuildItem additionalBeans() { @@ -103,7 +110,7 @@ ServiceStartBuildItem finalizeRouter( VertxHttpRecorder recorder, BeanContainerBuildItem beanContainer, Optional requireVirtual, InternalWebVertxBuildItem vertx, LaunchModeBuildItem launchMode, ShutdownContextBuildItem shutdown, - List defaultRoutes, List filters, + List defaultRoutes, List filters, FailureBuildItem failure, VertxWebRouterBuildItem router, EventLoopCountBuildItem eventLoopCount, HttpBuildTimeConfig httpBuildTimeConfig, HttpConfiguration httpConfiguration, BuildProducer reflectiveClass, List websocketSubProtocols) @@ -126,7 +133,7 @@ ServiceStartBuildItem finalizeRouter( recorder.finalizeRouter(beanContainer.getValue(), defaultRoute.map(DefaultRouteBuildItem::getRoute).orElse(null), - listOfFilters, vertx.getVertx(), router.getRouter(), httpBuildTimeConfig.rootPath, launchMode.getLaunchMode()); + listOfFilters, failure.getHandler(), vertx.getVertx(), router.getRouter(), httpBuildTimeConfig.rootPath, launchMode.getLaunchMode()); boolean startVirtual = requireVirtual.isPresent() || httpBuildTimeConfig.virtual; if (startVirtual) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index 2016bfc2bdda53..c9dd0db1022aab 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -9,6 +9,7 @@ import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.quarkus.vertx.http.runtime.cors.CORSConfig; +import io.quarkus.vertx.http.runtime.error.FailureConfig; @ConfigRoot(phase = ConfigPhase.RUN_TIME) public class HttpConfiguration { @@ -53,6 +54,11 @@ public class HttpConfiguration { * The CORS config */ public CORSConfig cors; + + /** + * The Failure config + */ + public FailureConfig failure; /** * The SSL config 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 deleted file mode 100644 index fbcd7f668990ac..00000000000000 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java +++ /dev/null @@ -1,112 +0,0 @@ -package io.quarkus.vertx.http.runtime; - -import static org.jboss.logging.Logger.getLogger; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicLong; - -import org.jboss.logging.Logger; - -import io.netty.handler.codec.http.HttpHeaderNames; -import io.quarkus.runtime.TemplateHtmlBuilder; -import io.vertx.core.Handler; -import io.vertx.ext.web.RoutingContext; - -public class QuarkusErrorHandler implements Handler { - - private static final Logger log = getLogger(QuarkusErrorHandler.class); - - /** - * we don't want to generate a new UUID each time as it is slowish. Instead we just generate one based one - * and then use a counter. - */ - private static final String BASE_ID = UUID.randomUUID().toString() + "-"; - - private static final AtomicLong ERROR_COUNT = new AtomicLong(); - - private final boolean showStack; - - public QuarkusErrorHandler(boolean showStack) { - this.showStack = showStack; - } - - @Override - public void handle(RoutingContext event) { - if (event.failure() == null) { - event.response().setStatusCode(event.statusCode()); - event.response().end(); - return; - } - event.response().setStatusCode(500); - String uuid = BASE_ID + ERROR_COUNT.incrementAndGet(); - String details = ""; - String stack = ""; - Throwable exception = event.failure(); - if (showStack && exception != null) { - details = generateHeaderMessage(exception, uuid); - stack = generateStackTrace(exception); - - } else { - details += "Error id " + uuid; - } - if (event.failure() instanceof IOException) { - log.debugf(exception, - "IOError processing HTTP request to %s failed, the client likely terminated the connection. Error id: %s", - event.request().uri(), uuid); - } else { - log.errorf(exception, "HTTP Request to %s failed, error id: %s", event.request().uri(), uuid); - } - String accept = event.request().getHeader("Accept"); - if (accept != null && accept.contains("application/json")) { - event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8"); - String escapedStack = stack.replace(System.lineSeparator(), "\\n").replace("\"", "\\\""); - StringBuilder jsonPayload = new StringBuilder("{\"details\":\"").append(details).append("\",\"stack\":\"") - .append(escapedStack).append("\"}"); - event.response().end(jsonPayload.toString()); - } else { - //We default to HTML representation - event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8"); - final TemplateHtmlBuilder htmlBuilder = new TemplateHtmlBuilder("Internal Server Error", details, details); - if (showStack && exception != null) { - htmlBuilder.stack(exception); - } - event.response().end(htmlBuilder.toString()); - } - } - - private static String generateStackTrace(final Throwable exception) { - StringWriter stringWriter = new StringWriter(); - exception.printStackTrace(new PrintWriter(stringWriter)); - - return escapeHtml(stringWriter.toString().trim()); - } - - private static String generateHeaderMessage(final Throwable exception, String uuid) { - return escapeHtml(String.format("Error handling %s, %s: %s", uuid, exception.getClass().getName(), - extractFirstLine(exception.getMessage()))); - } - - private static String extractFirstLine(final String message) { - if (null == message) { - return ""; - } - - String[] lines = message.split("\\r?\\n"); - return lines[0].trim(); - } - - private static String escapeHtml(final String bodyText) { - if (bodyText == null) { - return "null"; - } - - return bodyText - .replace("&", "&") - .replace("<", "<") - .replace(">", ">"); - } - -} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index ccdbdc3c4a8d52..df43381c83b041 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -168,7 +168,7 @@ public void startServer(RuntimeValue vertxRuntimeValue, ShutdownContext s } public void finalizeRouter(BeanContainer container, Consumer defaultRouteHandler, - List filterList, RuntimeValue vertx, + List filterList, Handler failureHandler, RuntimeValue vertx, RuntimeValue runtimeValue, String rootPath, LaunchMode launchMode) { // install the default route at the end Router router = runtimeValue.getValue(); @@ -198,7 +198,7 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute } container.instance(RouterProducer.class).initialize(resumingRouter); - router.route().last().failureHandler(new QuarkusErrorHandler(launchMode.isDevOrTest())); + router.route().last().failureHandler(failureHandler); if (rootPath.equals("/")) { if (hotReplacementHandler != null) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/error/ErrorRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/error/ErrorRecorder.java new file mode 100644 index 00000000000000..fcf6cc9bb8ed13 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/error/ErrorRecorder.java @@ -0,0 +1,20 @@ +package io.quarkus.vertx.http.runtime.error; + +import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.vertx.http.runtime.HttpConfiguration; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +@Recorder +public class ErrorRecorder { + + public Handler errorHandler(HttpConfiguration configuration, LaunchMode launchMode) { + if (configuration.failure.handler.isPresent()) { + //todo manage handler from config + return null; + } else { + return new QuarkusErrorHandler(launchMode.isDevOrTest()); + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/error/FailureConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/error/FailureConfig.java new file mode 100644 index 00000000000000..12190cff111d7c --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/error/FailureConfig.java @@ -0,0 +1,23 @@ +package io.quarkus.vertx.http.runtime.error; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class FailureConfig { + + /** + * Failure handler + */ + @ConfigItem + public Optional handler; + + @Override + public String toString() { + return "FailureConfig{" + + "handler=" + handler + + '}'; + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/error/QuarkusErrorHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/error/QuarkusErrorHandler.java new file mode 100644 index 00000000000000..1353051e94a871 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/error/QuarkusErrorHandler.java @@ -0,0 +1,229 @@ +package io.quarkus.vertx.http.runtime.error; + +import static org.jboss.logging.Logger.getLogger; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import org.jboss.logging.Logger; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.quarkus.runtime.TemplateHtmlBuilder; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class QuarkusErrorHandler implements Handler { + + private static final Logger log = getLogger(QuarkusErrorHandler.class); + + /** + * we don't want to generate a new UUID each time as it is slowish. Instead we just generate one based one + * and then use a counter. + */ + private static final String BASE_ID = UUID.randomUUID().toString() + "-"; + + private static final AtomicLong ERROR_COUNT = new AtomicLong(); + + private final boolean showStack; + + private volatile static List servletMappings = Collections.emptyList(); + private volatile static List staticResources = Collections.emptyList(); + private volatile static List additionalEndpoints = Collections.emptyList(); + private volatile static List descriptions = Collections.emptyList(); + + public static final class MethodDescription { + public String verb; + public String path; + public String produces; + public String consumes; + + public MethodDescription(String verb, String path, String produces, String consumes) { + super(); + this.verb = verb; + this.path = path; + this.produces = produces; + this.consumes = consumes; + } + } + + public static final class ResourceDescription { + public final String basePath; + public final List calls; + + public ResourceDescription(String basePath) { + this.basePath = basePath; + this.calls = new ArrayList<>(); + } + + public void addMethod(String verb, String path, String produces, String consumes) { + calls.add(new MethodDescription(verb, path, produces, consumes)); + } + } + + public QuarkusErrorHandler(boolean showStack) { + this.showStack = showStack; + } + + @Override + public void handle(RoutingContext event) { + if (event.failure() == null) { + if(HttpResponseStatus.NOT_FOUND.code() == event.statusCode()) { + handleNotFound(event); + } else { + event.response().setStatusCode(event.statusCode()); + event.response().end(); + } + return; + } + event.response().setStatusCode(500); + String uuid = BASE_ID + ERROR_COUNT.incrementAndGet(); + String details = ""; + String stack = ""; + Throwable exception = event.failure(); + if (showStack && exception != null) { + details = generateHeaderMessage(exception, uuid); + stack = generateStackTrace(exception); + + } else { + details += "Error id " + uuid; + } + if (event.failure() instanceof IOException) { + log.debugf(exception, + "IOError processing HTTP request to %s failed, the client likely terminated the connection. Error id: %s", + event.request().uri(), uuid); + } else { + log.errorf(exception, "HTTP Request to %s failed, error id: %s", event.request().uri(), uuid); + } + String accept = event.request().getHeader(HttpHeaderNames.ACCEPT); + if (accept != null && accept.contains(HttpHeaderValues.APPLICATION_JSON)) { + event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8"); + String escapedStack = stack.replace(System.lineSeparator(), "\\n").replace("\"", "\\\""); + StringBuilder jsonPayload = new StringBuilder("{\"details\":\"").append(details).append("\",\"stack\":\"") + .append(escapedStack).append("\"}"); + event.response().end(jsonPayload.toString()); + } else { + //We default to HTML representation + event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8"); + final TemplateHtmlBuilder htmlBuilder = new TemplateHtmlBuilder("Internal Server Error", details, details); + if (showStack && exception != null) { + htmlBuilder.stack(exception); + } + event.response().end(htmlBuilder.toString()); + } + } + + private static void handleNotFound(RoutingContext event) { + String accept = event.request().getHeader(HttpHeaderNames.ACCEPT); + if (accept != null && accept.contains(HttpHeaderValues.APPLICATION_JSON)) { + event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8"); + + } else { + //We default to HTML representation + event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8"); + TemplateHtmlBuilder sb = new TemplateHtmlBuilder("404 - Resource Not Found", "", "Resources overview"); + sb.resourcesStart("REST resources"); + for (ResourceDescription resource : descriptions) { + sb.resourcePath(resource.basePath); + for (MethodDescription method : resource.calls) { + sb.method(method.verb, method.path); + if (method.consumes != null) { + sb.consumes(method.consumes); + } + if (method.produces != null) { + sb.produces(method.produces); + } + sb.methodEnd(); + } + sb.resourceEnd(); + } + if (descriptions.isEmpty()) { + sb.noResourcesFound(); + } + sb.resourcesEnd(); + + if (!servletMappings.isEmpty()) { + sb.resourcesStart("Servlet mappings"); + for (String servletMapping : servletMappings) { + sb.servletMapping(servletMapping); + } + sb.resourcesEnd(); + } + + if (!staticResources.isEmpty()) { + sb.resourcesStart("Static resources"); + for (String staticResource : staticResources) { + sb.staticResourcePath(staticResource); + } + sb.resourcesEnd(); + } + + if (!additionalEndpoints.isEmpty()) { + sb.resourcesStart("Additional endpoints"); + for (String additionalEndpoint : additionalEndpoints) { + sb.staticResourcePath(additionalEndpoint); + } + sb.resourcesEnd(); + } + event.response().end(sb.toString()); + } + } + + private static String generateStackTrace(final Throwable exception) { + StringWriter stringWriter = new StringWriter(); + exception.printStackTrace(new PrintWriter(stringWriter)); + + return escapeHtml(stringWriter.toString().trim()); + } + + private static String generateHeaderMessage(final Throwable exception, String uuid) { + return escapeHtml(String.format("Error handling %s, %s: %s", uuid, exception.getClass().getName(), + extractFirstLine(exception.getMessage()))); + } + + private static String extractFirstLine(final String message) { + if (null == message) { + return ""; + } + + String[] lines = message.split("\\r?\\n"); + return lines[0].trim(); + } + + private static String escapeHtml(final String bodyText) { + if (bodyText == null) { + return "null"; + } + + return bodyText + .replace("&", "&") + .replace("<", "<") + .replace(">", ">"); + } + + public static void servlets(Map> servletToMapping) { + QuarkusErrorHandler.servletMappings = servletToMapping.values().stream() + .flatMap(List::stream) + .sorted() + .collect(Collectors.toList()); + } + + public static void staticResources(Set knownFiles) { + QuarkusErrorHandler.staticResources = knownFiles.stream().sorted().collect(Collectors.toList()); + } + + public static void setAdditionalEndpoints(List additionalEndpoints) { + QuarkusErrorHandler.additionalEndpoints = additionalEndpoints; + } + +}