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

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

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.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
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.TemplateHtmlBuilder;
import io.quarkus.security.AuthenticationFailedException;
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);

    /**
     * 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 final Optional contentTypeDefault;

    public QuarkusErrorHandler(boolean showStack, Optional contentTypeDefault) {
        this.showStack = showStack;
        this.contentTypeDefault = contentTypeDefault;
    }

    @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 AuthenticationFailedException) {
                //generally this should be handled elsewhere
                //but if we get to this point bad things have happened
                //so it is better to send a response than to hang
                event.response().setStatusCode(HttpResponseStatus.UNAUTHORIZED.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 lets 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);
                break;
            default:
                // We default to JSON representation
                switch (contentTypeDefault.orElse(HttpConfiguration.PayloadHint.JSON)) {
                    case HTML:
                        htmlResponse(event, details, exception);
                        break;
                    case JSON:
                    default:
                        jsonResponse(event, ContentTypes.APPLICATION_JSON, details, stack);
                        break;
                }
                break;
        }
    }

    private void jsonResponse(RoutingContext event, String contentType, String details, String stack) {
        event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, contentType + "; charset=utf-8");
        String escapedDetails = escapeJsonString(details);
        String escapedStack = escapeJsonString(stack);
        StringBuilder jsonPayload = new StringBuilder("{\"details\":\"")
                .append(escapedDetails)
                .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("Internal Server Error", details, details);
        if (showStack && exception != null) {
            htmlBuilder.stack(exception);
        }
        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 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 Collection SUPPORTED = Arrays.asList(
                new ParsableMIMEValue(APPLICATION_JSON).forceParse(),
                new ParsableMIMEValue(TEXT_JSON).forceParse(),
                new ParsableMIMEValue(TEXT_HTML).forceParse(),
                new ParsableMIMEValue(APPLICATION_XHTML).forceParse(),
                new ParsableMIMEValue(APPLICATION_XML).forceParse(),
                new ParsableMIMEValue(TEXT_XML).forceParse());

        static String pickFirstSupportedAndAcceptedContentType(RoutingContext context) {
            List acceptableTypes = context.parsedHeaders().accept();
            MIMEHeader result = context.parsedHeaders().findBestUserAcceptedIn(acceptableTypes, SUPPORTED);
            return result == null ? null : result.value();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy