When streaming response of unknown size one encounters error in the middle of the stream, there is a need to break the connection such that download will fail on the client side.
This just works in pure servlet environment - throwing exception will abort connection without writing final 0
which clients will interpret as broken download.
Under Spring Boot however exact same code, whether ran as rest controller or managed servlet, will be implicitly closed with a trailing 0
and thus the client will finish download without error even though data was incomplete.
Example code: https://gist.github.com/mwisnicki/6df71b2b5f7712306fca27bf34c24e94 Relevant discussion on tomcat-users: https://lists.apache.org/thread.html/r824baf5587136f99a764ff7ebc10d9e353b3fba71167d2506cadfb8d%40%3Cusers.tomcat.apache.org%3E
Comment From: wilkinsona
Thanks for the report but I'm not sure that there's much that we can do about this. Tomcat behaves in the same way if you register a custom error page:
```java static class ErrorServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (!resp.isCommitted()) {
resp.getWriter().println("Custom error response");
}
}
}
static class TomcatApp {
public static void main(String[] args) throws Exception {
var tomcat = new Tomcat();
tomcat.setPort(8080);
Context context = tomcat.addContext("", new File(".").getAbsolutePath());
ErrorPage errorPage = new ErrorPage();
errorPage.setLocation("/error");
context.addErrorPage(errorPage);
tomcat.addServlet("", "demo", new DemoServlet()).addMapping("/servlet");
tomcat.addServlet("", "error", new ErrorServlet()).addMapping("/error");
tomcat.getConnector(); // FFS
tomcat.start();
tomcat.getServer().await();
}
}
```
In this case the response has been committed so the error servlet does nothing. This isn't exactly the same as Spring MVC's behaviour, but it results in Tomcat taking the same code path and calling finishResponse
. It's this call which causes the unwanted trailing 0
to be sent.
Excluding ErrorMvcAutoConfiguration
(using the spring.autoconfigure.exclude
property, for example), switches off Boot's registration of a custom error page which prevents the 0
from being sent.
I'm not sure there's much more that we can do here and suspect that anything better would require a change to Tomcat's custom error page support. FWIW, Jetty behaves as you would like without excluding ErrorMvcAutoConfiguration
which further reinforces my feeling that this needs to be tackled in Tomcat.
What do you think, @markt-asf?
Comment From: markt-asf
Tomcat handles custom error pages for a committed response by including the custom error page content at the end of the response. The idea was that this would provide some sort of readable error message for human users. For automated clients it would break the expected format of the response thereby signalling an error despite the 200 status code.
If the output of the custom error page is conditional on the response not being committed then all the client will see is a truncated response that may or may not be obvious as an error condition.
What I think we can do is modify StandardHostValve
so that in the committed case, we still trigger an immediate close (without the terminating end chunk) after writing the error response as an additional signal to the client that the response is broken.
I have an implementation of this. I need to do further testing before committing.
Comment From: philwebb
@markt-asf Do you have a bugzilla link for those changes that you could add here?
Comment From: markt-asf
https://github.com/apache/tomcat/commit/fa0f2567aae91a13137094ebcdb74d72b8222f3b (with appropriate back-ports to all currently supported versions)
Comment From: wilkinsona
Thanks very much, Mark.
Comment From: mwisnicki
Thanks, that was fast!