'Force Tomcat a JSON response of BadRequest error: Invalid character found in the request target

I have this spring boot (v2.2.8) app for REST-full service, and I want all responses and errors to be only in JSON.

Almost every error works with @ExceptionHandler and @RestControllerAdvice, except from exception that seems to be directly from tomcat:

java.lang.IllegalArgumentException: Invalid character found in the request target [/some/url/with/"invalid/char]. The valid characters are defined in RFC 7230 and RFC 3986
at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:499) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:260) [tomcat-embed-core-9.0.36.jar:9.0.36]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.36.jar:9.0.36]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-9.0.36.jar:9.0.36]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590) [tomcat-embed-core-9.0.36.jar:9.0.36]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.36.jar:9.0.36]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) [?:?]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) [?:?]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.36.jar:9.0.36]
    at java.lang.Thread.run(Thread.java:834) [?:?]

I know about many answers here on this topic, but I do not want to allow special characters. All I want is to return error response in JSON, instead of this HTML page.

<!doctype html><html lang="en"><head><title>HTTP Status 400 – Bad Request</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 400 – Bad Request</h1></body></html>%

I can't find any solution on how to customise tomcat error page to return JSON format. Also basic spring boot error handling is ignored with this specific error, I suppose because it is thrown much sooner in process on tomcat level and spring doesn't even know about it?

Is there a way that tomcat will default all responses to JSON?



Solution 1:[1]

There is an issue with spring boot that is about customizing the white label HTML page: issue with example

The solution is to add a custom error valve to your spring context:

public class ApiConfiguration implements WebServerFactoryCustomizer<ConfigurableTomcatWebServerFactory> {

    @Override
    public void customize(ConfigurableTomcatWebServerFactory factory) {

        factory.addContextCustomizers((context) -> {
            Container parent = context.getParent();
            if (parent instanceof StandardHost) {

                ((StandardHost) parent).setErrorReportValveClass(CustomTomcatErrorValve.class.getName());
            }
        });
    }
}

Then you can do the following in the error valve:

public class CustomTomcatErrorValve extends ErrorReportValve {

    private static final Logger logger = LoggerFactory.getLogger(CustomTomcatErrorValve.class);

    @Override
    protected void report(Request request, Response response, Throwable throwable) {

        if (!response.setErrorReported())
            return;

        if (logger.isDebugEnabled())
            logger.debug("Tomcat failed to prepare the request for spring (set response code to {}).", response.getStatus(), throwable);

        HttpStatus status = HttpStatus.valueOf(response.getStatus());

        try {

            response.setContentType("application/problem+json");
            Writer writer = response.getReporter();
            writer.write(String.format("""
                    {
                        "title": "%s",
                        "status": %d
                    }""", status.getReasonPhrase(), status.value()));
            response.finishResponse();
        } catch (IOException ignored) {
        }
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Johannes