All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.quarkus.vertx.http.runtime.QuarkusErrorHandler Maven / Gradle / Ivy

The newest version!
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.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;

import org.jboss.logging.Logger;

import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.quarkus.runtime.ErrorPageAction;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.TemplateHtmlBuilder;
import io.quarkus.runtime.logging.DecorateStackUtil;
import io.quarkus.security.AuthenticationCompletionException;
import io.quarkus.security.AuthenticationException;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.UnauthorizedException;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticator;
import io.vertx.core.Handler;
import io.vertx.ext.web.MIMEHeader;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.impl.ParsableMIMEValue;

public class QuarkusErrorHandler implements Handler {

    private static final Logger log = getLogger(QuarkusErrorHandler.class);
    private static final String NL = "\n";
    private static final String TAB = "\t";
    private static final String HEADING = "500 - Internal Server Error";

    /**
     * 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() + "-";

    private static final AtomicLong ERROR_COUNT = new AtomicLong();

    private final boolean showStack;
    private final boolean decorateStack;
    private final Optional contentTypeDefault;
    private final List actions;
    private final List knowClasses;
    private final String srcMainJava;

    public QuarkusErrorHandler(boolean showStack, boolean decorateStack,
            Optional contentTypeDefault) {
        this(showStack, decorateStack, contentTypeDefault, null, List.of(), List.of());
    }

    public QuarkusErrorHandler(boolean showStack, boolean decorateStack,
            Optional contentTypeDefault,
            String srcMainJava,
            List knowClasses,
            List actions) {
        this.showStack = showStack;
        this.decorateStack = decorateStack;
        this.contentTypeDefault = contentTypeDefault;
        this.srcMainJava = srcMainJava;
        this.knowClasses = knowClasses;
        this.actions = actions;
    }

    @Override
    public void handle(RoutingContext event) {
        try {
            if (event.failure() == null) {
                event.response().setStatusCode(event.statusCode());
                event.response().end();
                return;
            }
            //this can happen if there is no auth mechanisms
            if (event.failure() instanceof UnauthorizedException) {
                HttpAuthenticator authenticator = event.get(HttpAuthenticator.class.getName());
                if (authenticator != null) {
                    authenticator.sendChallenge(event).subscribe().with(new Consumer<>() {
                        @Override
                        public void accept(Boolean aBoolean) {
                            event.response().end();
                        }
                    }, new Consumer<>() {
                        @Override
                        public void accept(Throwable throwable) {
                            event.fail(throwable);
                        }
                    });
                } else {
                    event.response().setStatusCode(HttpResponseStatus.UNAUTHORIZED.code()).end();
                }
                return;
            }
            if (event.failure() instanceof ForbiddenException) {
                event.response().setStatusCode(HttpResponseStatus.FORBIDDEN.code()).end();
                return;
            }

            if (event.failure() instanceof AuthenticationException) {
                if (event.response().getStatusCode() == HttpResponseStatus.OK.code()) {
                    //set 401 if status wasn't set upstream
                    event.response().setStatusCode(HttpResponseStatus.UNAUTHORIZED.code());
                }

                //when proactive security is enabled and this wasn't handled elsewhere, we expect event to
                //end here as failing event makes it possible to customize response, however when proactive security is
                //disabled, this should be handled elsewhere and if we get to this point bad things have happened,
                //so it is better to send a response than to hang

                if (event.failure() instanceof AuthenticationCompletionException
                        && event.failure().getMessage() != null
                        && LaunchMode.current() == LaunchMode.DEVELOPMENT) {
                    event.response().end(event.failure().getMessage());
                } else {
                    event.response().end();
                }
                return;
            }

            if (event.failure() instanceof RejectedExecutionException) {
                log.warn(
                        "Worker thread pool exhaustion, no more worker threads available - returning a `503 - SERVICE UNAVAILABLE` response.");
                event.response().setStatusCode(HttpResponseStatus.SERVICE_UNAVAILABLE.code()).end();
                return;
            }
        } catch (IllegalStateException e) {
            //ignore this if the response is already started
            if (!event.response().ended()) {
                //could be that just the head is committed
                event.response().end();
            }
            return;
        }

        if (!event.response().headWritten()) {
            event.response().setStatusCode(event.statusCode() > 0 ? event.statusCode() : 500);
        }

        String uuid = BASE_ID + ERROR_COUNT.incrementAndGet();
        String details;
        String stack = "";
        Throwable exception = event.failure();
        String responseContentType = null;
        try {
            responseContentType = ContentTypes.pickFirstSupportedAndAcceptedContentType(event);
        } catch (RuntimeException e) {
            // Let's shield ourselves from bugs in this parsing code:
            // we're already handling an exception,
            // so the priority is to return *something* to the user.
            // If we can't pick the appropriate content-type, well, so be it.
            exception.addSuppressed(e);
        }
        if (showStack && exception != null) {
            details = generateHeaderMessage(exception, uuid);
            stack = generateStackTrace(exception);
        } else {
            details = generateHeaderMessage(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);
        }
        //we have logged the error
        //now let's see if we can actually send a response
        //if not we just return
        if (event.response().ended()) {
            return;
        } else if (event.response().headWritten()) {
            event.response().end();
            return;
        }

        if (responseContentType == null) {
            responseContentType = "";
        }

        switch (responseContentType) {
            case ContentTypes.TEXT_HTML:
            case ContentTypes.APPLICATION_XHTML:
            case ContentTypes.APPLICATION_XML:
            case ContentTypes.TEXT_XML:
                htmlResponse(event, details, exception);
                break;
            case ContentTypes.APPLICATION_JSON:
            case ContentTypes.TEXT_JSON:
                jsonResponse(event, responseContentType, details, stack, exception);
                break;
            case ContentTypes.TEXT_PLAIN:
                textResponse(event, details, stack, exception);
                break;
            default:
                if (contentTypeDefault.isPresent()) {
                    switch (contentTypeDefault.get()) {
                        case HTML:
                            htmlResponse(event, details, exception);
                            break;
                        case JSON:
                            jsonResponse(event, ContentTypes.APPLICATION_JSON, details, stack, exception);
                            break;
                        case TEXT:
                            textResponse(event, details, stack, exception);
                            break;
                        default:
                            defaultResponse(event, details, stack, exception);
                            break;
                    }
                } else {
                    defaultResponse(event, details, stack, exception);
                    break;
                }
                break;
        }
    }

    private void defaultResponse(RoutingContext event, String details, String stack, Throwable throwable) {
        String userAgent = event.request().getHeader("User-Agent");
        if (userAgent != null && (userAgent.toLowerCase(Locale.ROOT).startsWith("wget/")
                || userAgent.toLowerCase(Locale.ROOT).startsWith("curl/"))) {
            textResponse(event, details, stack, throwable);
        } else {
            jsonResponse(event, ContentTypes.APPLICATION_JSON, details, stack, throwable);
        }
    }

    private void textResponse(RoutingContext event, String details, String stack, Throwable throwable) {
        event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, ContentTypes.TEXT_PLAIN + "; charset=utf-8");
        String decoratedString = null;
        if (decorateStack && throwable != null) {
            decoratedString = DecorateStackUtil.getDecoratedString(throwable, srcMainJava, knowClasses);
        }

        try (StringWriter sw = new StringWriter()) {
            sw.write(NL + HEADING + NL);
            sw.write("---------------------------" + NL);
            sw.write(NL);
            sw.write("Details:");
            sw.write(NL);
            sw.write(TAB + details);
            sw.write(NL);
            if (decoratedString != null) {
                sw.write("Decorate (Source code):");
                sw.write(NL);
                sw.write(TAB + decoratedString);
                sw.write(NL);
            }
            sw.write("Stack:");
            sw.write(NL);
            sw.write(TAB + stack);
            sw.write(NL);
            writeResponse(event, sw.toString());
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    private void jsonResponse(RoutingContext event, String contentType, String details, String stack, Throwable throwable) {
        event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, contentType + "; charset=utf-8");
        String escapedDetails = escapeJsonString(details);
        String escapedStack = escapeJsonString(stack);
        String decoratedString = null;
        if (decorateStack && throwable != null) {
            decoratedString = DecorateStackUtil.getDecoratedString(throwable, srcMainJava, knowClasses);
        }

        StringBuilder jsonPayload = new StringBuilder("{\"details\":\"")
                .append(escapedDetails);

        if (decoratedString != null) {
            jsonPayload = jsonPayload.append("\",\"decorate\":\"")
                    .append(escapeJsonString(decoratedString));
        }

        jsonPayload = jsonPayload.append("\",\"stack\":\"")
                .append(escapedStack)
                .append("\"}");
        writeResponse(event, jsonPayload.toString());
    }

    private void htmlResponse(RoutingContext event, String details, Throwable exception) {
        event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8");
        final TemplateHtmlBuilder htmlBuilder = new TemplateHtmlBuilder(showStack, "Internal Server Error", details, details,
                this.actions);

        if (decorateStack && exception != null) {
            htmlBuilder.decorate(exception, this.srcMainJava, this.knowClasses);
        }
        if (showStack && exception != null) {
            htmlBuilder.stack(exception, this.knowClasses);
        }

        writeResponse(event, htmlBuilder.toString());
    }

    private void writeResponse(RoutingContext event, String output) {
        if (!event.response().ended()) {
            event.response().end(output);
        }
    }

    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, String uuid) {
        return String.format("Error id %s, %s: %s", uuid, exception.getClass().getName(),
                extractFirstLine(exception.getMessage()));
    }

    private static String generateHeaderMessage(String uuid) {
        return String.format("Error id %s", uuid);
    }

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

        String[] lines = message.split("\\r?\\n");
        return lines[0].trim();
    }

    static String escapeJsonString(final String text) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < text.length(); i++) {
            char ch = text.charAt(i);
            switch (ch) {
                case '"':
                    sb.append("\\\"");
                    break;
                case '\\':
                    sb.append("\\\\");
                    break;
                case '\b':
                    sb.append("\\b");
                    break;
                case '\f':
                    sb.append("\\f");
                    break;
                case '\n':
                    sb.append("\\n");
                    break;
                case '\r':
                    sb.append("\\r");
                    break;
                case '\t':
                    sb.append("\\t");
                    break;
                default:
                    sb.append(ch);
            }
        }
        return sb.toString();
    }

    private static final class ContentTypes {

        private ContentTypes() {
        }

        private static final String APPLICATION_JSON = "application/json";
        private static final String TEXT_JSON = "text/json";
        private static final String TEXT_HTML = "text/html";
        private static final String TEXT_PLAIN = "text/plain";
        private static final String APPLICATION_XHTML = "application/xhtml+xml";
        private static final String APPLICATION_XML = "application/xml";
        private static final String TEXT_XML = "text/xml";

        // WARNING: The order matters for wildcards: if text/json is before text/html, then text/* will match text/json.
        private static final List BASE_HEADERS = List.of(
                createParsableMIMEValue(APPLICATION_JSON),
                createParsableMIMEValue(TEXT_JSON),
                createParsableMIMEValue(TEXT_HTML),
                createParsableMIMEValue(APPLICATION_XHTML),
                createParsableMIMEValue(APPLICATION_XML),
                createParsableMIMEValue(TEXT_XML));

        private static final Collection SUPPORTED = createSupported();
        private static final Collection SUPPORTED_CURL = createSupportedCurl();

        private static Collection createSupported() {
            var supported = new ArrayList(BASE_HEADERS.size() + 1);
            supported.addAll(BASE_HEADERS);
            supported.add(createParsableMIMEValue(TEXT_PLAIN));
            return Collections.unmodifiableCollection(supported);
        }

        private static Collection createSupportedCurl() {
            var supportedCurl = new ArrayList(BASE_HEADERS.size() + 1);
            supportedCurl.add(createParsableMIMEValue(TEXT_PLAIN));
            supportedCurl.addAll(BASE_HEADERS);
            return Collections.unmodifiableCollection(supportedCurl);
        }

        private static ParsableMIMEValue createParsableMIMEValue(String applicationJson) {
            return new ParsableMIMEValue(applicationJson).forceParse();
        }

        static String pickFirstSupportedAndAcceptedContentType(RoutingContext context) {
            List acceptableTypes = context.parsedHeaders().accept();

            String userAgent = context.request().getHeader("User-Agent");
            if (userAgent != null && (userAgent.toLowerCase(Locale.ROOT).startsWith("wget/")
                    || userAgent.toLowerCase(Locale.ROOT).startsWith("curl/"))) {
                MIMEHeader result = context.parsedHeaders().findBestUserAcceptedIn(acceptableTypes, SUPPORTED_CURL);
                return result == null ? null : result.value();
            } else {
                MIMEHeader result = context.parsedHeaders().findBestUserAcceptedIn(acceptableTypes, SUPPORTED);
                return result == null ? null : result.value();
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy