We'd like to investigate a little more to find out why the ErrorReportValve is being used and the DispatcherServlet isn't being called at all.
This happens when the failure occurs before the request has been matched to a particular context. An illegal HTTP header or a malformed URL will both cause such a failure.
With Jetty, a request with an illegal HTTP header produces the following response:
HTTP/1.1 400 Illegal character VCHAR='('
Content-Type: text/html;charset=iso-8859-1
Content-Length: 70
Connection: close
<h1>Bad Message 400</h1><pre>reason: Illegal character VCHAR='('</pre>
Undertow produces the following response:
HTTP/1.1 400 Bad Request
Content-Length: 0
Connection: close
If we can't route into the app and its error pages, Undertow's response seems like the best alternative as there's nothing in it that could be used to identify the server that is producing the response.
The response body can be suppressed when using Jetty by configuring the Server
with a custom ErrorHandler
:
@Bean
JettyServerCustomizer serverCustomizer() {
return (server) -> {
server.setErrorHandler(new ErrorHandler() {
@Override
public ByteBuffer badMessageError(int status, String reason, HttpFields fields) {
return null;
We can't do anything about the reason in the status line as the logic that generates it isn't pluggable.
I hit this issue with a request header that was too large.
Looks like I am able to get around the default configuration like this, without any changes to Spring. The key is the setErrorReportValveClass(), and the getOrder().
@Bean
public TomcatWebSocketServletWebServerCustomizer errorValveCustomizer() {
return new TomcatWebSocketServletWebServerCustomizer() {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addContextCustomizers((context) -> {
Container parent = context.getParent();
if ( parent instanceof StandardHost) {
((StandardHost) parent).setErrorReportValveClass("aaa.bbb.ccc.configuration.CustomTomcatErrorValve");
@Override
public int getOrder() {
return 100; // needs to be AFTER the one configured with TomcatWebServerFactoryCustomizer
Then create that class aaa.bbb.ccc.configuration.CustomTomcatErrorValve
package aaa.bbb.ccc;
public class CustomTomcatErrorValve extends ErrorReportValve{
private static final Logger LOGGER = LoggerFactory.getLogger(CustomTomcatErrorValve.class);
protected void report(Request request, Response response, Throwable throwable) {
if (!response.setErrorReported())
return;
LOGGER.warn("{} Fatal error before getting to Spring. {} ", response.getStatus(), errorCode, throwable);
try {
Writer writer = response.getReporter();
writer.write(Integer.toString(response.getStatus()));
writer.write(" Fatal error. Could not process request.");
response.finishResponse();
} catch (IOException e) {
This valve can be configured however you'd like, BUT as discussed above, you don't have too many options because the context hasn't (may not have) been set in the Request yet, so you can't redirect and setting setProperty("errorCode.0", .... ) requires a File path. So here, I'm just returning the error text directly.
It's also possible to do this, instead of a direct response...
String errorCode = .....;
LOGGER.warn("{} Fatal error before getting to Spring. {} ", response.getStatus(), errorCode, throwable);
response.setStatus(302);
response.setHeader("Location", "/error?errorCode="+errorCode);
but.... I'm not sure that's better. If the tomcat request is dying so early that we can't do a normal redirect, or forward, than it's probably not really safe to expect the /error page will work either. But if you have an external error page you could reference, that could be nicer.
I could see Spring following the above to provide an internal Spring ErrorValve that generates the disaster HTML response, and then taking it from a server.error.disaster-html type property. Or even allowing a FunctionalInterface customizer that generates text,
Tomcat invokes ErrorReportValve directly for malformed URLs
Allow custom ErrorReportValve to be used with Tomcat and provide whitelabel version
Jun 1, 2021
For anyone trying @sc-moonlight's workaround (many thanks for this!), I had to add the following line to get it to work:
((StandardHost) parent).addValve(new CustomTomcatErrorValve());
@sc-moonlight's solution works fine as long as you don't apply any other customization.
In my case I previously had this WebServerFactoryCustomizer
:
@Slf4j
@Configuration
@ConditionalOnClass(Tomcat.class)
public class TomcatConfiguration implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.setTomcatContextCustomizers(Collections.singletonList(new CustomCustomizer()));
private static class CustomCustomizer implements TomcatContextCustomizer {
public void customize(Context context) {
context.setReloadable(false);
context.setMapperContextRootRedirectEnabled(false);
I added the mentioned TomcatWebSocketServletWebServerCustomizer
bean and it didn't work. I tried commenting out my previous customization and then the CustomTomcatErrorValve
was correctly applied:
@Override
public void customize(TomcatServletWebServerFactory factory) {
//factory.setTomcatContextCustomizers(Collections.singletonList(new CustomCustomizer()));
Any workaround to be able to apply both customizations?
I have also come across this issue also raised due to pen-test findings some time back. The solution provided by @sc-moonlight and @petenattress above worked well. Combining both and making it a @Component
, this is what we have so that the body is fully stripped out from the response:
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.valves.ErrorReportValve;
import org.springframework.boot.autoconfigure.websocket.servlet.TomcatWebSocketServletWebServerCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.stereotype.Component;
@Component
public class ErrorValveConfig extends TomcatWebSocketServletWebServerCustomizer {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addContextCustomizers((context) -> {
if (context.getParent() instanceof StandardHost parent) {
parent.setErrorReportValveClass(BlankBodyErrorReportValve.class.getName());
parent.addValve(new BlankBodyErrorReportValve());
@Override
public int getOrder() {
return 100; // needs to be AFTER the one configured with TomcatWebServerFactoryCustomizer
private static class BlankBodyErrorReportValve extends ErrorReportValve {
@Override
protected void report(Request request, Response response, Throwable throwable) {
response.setErrorReported();
We use that in conjunction with ExceptionHandlers for actual business logic exception handling.
Here is my approach to this issue, based on the others above:
@Bean
public TomcatWebServerFactoryCustomizer blankErrorValveTomcatWebServerFactoryCustomizer(
Environment environment,
ServerProperties serverProperties,
TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer
return new TomcatWebServerFactoryCustomizer(environment, serverProperties) {
@Override
public void customize(ConfigurableTomcatWebServerFactory factory) {
factory.addContextCustomizers( context -> {
if(context.getParent() instanceof StandardHost standardHost) {
standardHost.getPipeline().addValve(new BlankTomcatErrorValve());
standardHost.setErrorReportValveClass(BlankTomcatErrorValve.class.getName());
@Override
public int getOrder() {
return tomcatWebServerFactoryCustomizer.getOrder() + 1;
This should be pretty bulletproof. I put it in a @Configuration
and used this valve to disable any error responses:
public class BlankTomcatErrorValve extends ErrorReportValve {
private static final Logger LOGGER = LoggerFactory.getLogger(BlankTomcatErrorValve.class);
@Override
protected void report(Request request, Response response, Throwable throwable) {
LOGGER.warn("Fatal error in Servlet:", throwable);
response.setSuspended(true);
@wilkinsona @sc-moonlight @codependent @ClaudioConsolmagno @MayCXC
Is this still working for you in Spring-Boot 2.7.5 / Tomcat 9.0.68?
This was working for me for several versions over the past several years, including in Spring-Boot 2.7.4, but broke in 2.7.5. I've spent several hours looking at it today, but I'm not quite sure what happened.
You may not notice it, but the default behavior in the latest version seems to be to return an HTTP 400 and absolutely no body. This isn't the behavior I want - I want it to be an HTTP 500 + and the body should be a small hardcoded string.
I'm not sure whether this is an intended change (in which case, how are we supposed to customize this now?) or if Tomcat, Catalina, Spring-Boot, or something else has regressed.
Or is everything still working fine for the rest of you?
Edit: Spring-Boot 2.7.5 works for me as long as I downgrade Tomcat to 9.0.65 (the same version that came with 2.7.4). Downgrading to 9.0.67 didn't fix the issue and I wasn't able to download 9.0.66 - the release history shows that 9.0.67 was released less than a day after 9.0.66, so I assume that had a major issue and was pulled.
Is it a bug with Tomcat 9.0.67+ or Spring-Boot 2.7.5? Or is there just a different process now that I don't know about?
Edit 2x: I found the issue. It has nothing to do with Spring-Boot. Tomcat's ErrorReportValve was changed between 9.0.65 and 9.0.67. They moved some logic from the start of the report function to the end of the invoke function. Removing the call to "setErrorReported" from my report function appears to have fixed my issues.
@ClaudioConsolmagno - I believe you can actually remove the one line from your report function in 2.7.5.
The logic in the base ErrorReportValve class was rearranged. It now calls that one line just before calling report instead of just inside of it.
It returns a Boolean (whether the function was called previously or not). I had been checking the return value of that Boolean to decide whether to write out my fixed error string or not, but since they now call it for you, it was always telling my code to do nothing. I removed that check and now my code works in 2.7.5.