DEV Community

Kamil
Kamil

Posted on • Originally published at banach.net.pl on

How to customize Jetty in Spring - Custom Error Handler

Last week we found out that in some cases our application is showing redundant data on error - for example stacktrace. It happens when there was an error which was handled by the application container which was Jetty. For example, sending a header like X-FORWARDED-PORT: some-not-numeric-value causesNumberFormatException and shows a full stacktrace.

We’ve looked over the documentation and see that we could hide stacktrace in default error handler. Instead of that, we decided to replace it with a custom one. That allows us to put an additional logging logic and customize the output. And this is a short description of how to do it. ;-)

To implement the custom error handler we can extend org.eclipse.jetty.server.handler.AbstractHandler and override handle method. On the code snippet below there is an example handler. For sake of clarity it returns always simple text (content-type text/plain) with code, error message and URL used.

We can extend it by adding additional logic like different responses based on headers from the client, additional logging and so on. :-)

package pl.net.banach.customizeJetty;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.ByteBufferOutputStream;
import org.eclipse.jetty.server.Dispatcher;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.StringUtil;

public class CustomJettyErrorHandler extends AbstractHandler {

    @Override
    public void handle(String target, Request baseRequest, 
                       HttpServletRequest request,
                       HttpServletResponse response) 
                       throws IOException, ServletException {


        try {
            // Get error message, sanitize it, just in case.
            String message = StringUtil.sanitizeXmlString(
                (String) request.getAttribute(Dispatcher.ERROR_MESSAGE)
            );

            // Get error code that will returned
            int code = response.getStatus();

            var charset = StandardCharsets.UTF_8;

            // Get writer used
            var buffer = baseRequest.getResponse().getHttpOutput().getBuffer();
            var out = new ByteBufferOutputStream(buffer);
            var writer = new PrintWriter(new OutputStreamWriter(out, charset));

            // Set content type, encoding and write response
            response.setContentType(MimeTypes.Type.TEXT_PLAIN.asString());
            response.setCharacterEncoding(charset.name());
            writer.print("HTTP ERROR ");
            writer.print(code);
            writer.print("\nMessage: ");
            writer.print(message);
            writer.print("\nURI: ");
            writer.print(request.getRequestURI());


            writer.flush();
        } catch (BufferOverflowException e) {
            baseRequest.getResponse().resetContent();
        }

        baseRequest.getHttpChannel().sendResponseAndComplete();
    }
}
Enter fullscreen mode Exit fullscreen mode

When we have the error handler ready then it registering it is easy. To do this we will create a custom configuration which implements WebServerFactoryCustomizer with @Configuration annotation. In a customize method we will create a customizer and add a newly created error handler.

Seems simple, but be sure to put this configuration class in a package that is scanned by Spring @ComponentScan annotation!

package pl.net.banach.customizeJetty;

import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer;
import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JettyConfiguration
            implements WebServerFactoryCustomizer<JettyServletWebServerFactory> {

    @Override
    public void customize(JettyServletWebServerFactory factory) {
        JettyServerCustomizer customizer = server -> {
            server.setErrorHandler(new CustomJettyErrorHandler());
        };
        factory.addServerCustomizers(customizer);
    }
}
Enter fullscreen mode Exit fullscreen mode

And that’s all - from now all errors that go to Jetty (application server) will be handled by our custom handler. :-)

Top comments (0)