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
causes
NumberFormatException
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();
}
}
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);
}
}
And that’s all - from now all errors that go to Jetty (application server) will be handled by our custom handler. :-)